From 0571c871670512e6458b7e30f2d137b4cfb00932 Mon Sep 17 00:00:00 2001 From: Rob Dobson Date: Sun, 25 Oct 2026 19:21:11 +0100 Subject: [PATCH] Dashboard CameraPanel: show the captured snapshot After a capture the panel downloads the image file the device wrote (fsGetContents, with progress) and shows it with its size and filename, with Download and Close buttons. The image URL is revoked when replaced, closed or the panel unmounts. Brought over from the old feature/camera-panel branch. Co-Authored-By: Claude Opus 5.5 --- examples/dashboard/src/CameraPanel.tsx | 86 ++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/examples/dashboard/src/CameraPanel.tsx b/examples/dashboard/src/CameraPanel.tsx index 135ae0f..38fef51 100644 --- a/examples/dashboard/src/CameraPanel.tsx +++ b/examples/dashboard/src/CameraPanel.tsx @@ -43,6 +43,15 @@ interface CameraPanelProps { onSnapshotTaken?: () => void; } +// The last snapshot, downloaded from the device to show in the panel +interface Snapshot { + name: string; + url: string; + bytes: number; + width: number; + height: number; +} + export default function CameraPanel({ onSnapshotTaken }: CameraPanelProps) { const [status, setStatus] = useState(null); const [liveEnabled, setLiveEnabled] = useState(false); @@ -57,7 +66,10 @@ export default function CameraPanel({ onSnapshotTaken }: CameraPanelProps) { const [isBusy, setIsBusy] = useState(false); const [lastMsg, setLastMsg] = useState(''); const [lastError, setLastError] = useState(''); + const [snapshot, setSnapshot] = useState(null); + const [snapshotProgress, setSnapshotProgress] = useState(null); const imageUrlRef = useRef(null); + const snapshotUrlRef = useRef(null); const liveEnabledRef = useRef(false); const fetchStatus = async () => { @@ -156,6 +168,56 @@ export default function CameraPanel({ onSnapshotTaken }: CameraPanelProps) { }; }, []); + // Release the snapshot image when the panel goes + useEffect(() => { + return () => { + if (snapshotUrlRef.current) URL.revokeObjectURL(snapshotUrlRef.current); + }; + }, []); + + // Show a snapshot (or none), releasing the one shown before + const showSnapshot = (shot: Snapshot | null) => { + if (snapshotUrlRef.current) URL.revokeObjectURL(snapshotUrlRef.current); + snapshotUrlRef.current = shot ? shot.url : null; + setSnapshot(shot); + }; + + // Download a snapshot just taken from the device and show it + const loadSnapshot = async (fsName: string, filename: string, width: number, height: number) => { + const name = filename.replace(/^.*\//, ''); + setSnapshotProgress(0); + try { + const result = await connManager.getConnector().fsGetContents( + `${fsName}/${filename.replace(/^\/+/, '')}`, + 'fs', + (received: number, total: number) => { + if (total > 0) setSnapshotProgress(Math.round((received / total) * 100)); + } + ); + if (result.downloadedOk && result.fileData) { + const copy = new Uint8Array(result.fileData); + const blob = new Blob([copy.buffer as ArrayBuffer], { type: 'image/jpeg' }); + showSnapshot({ name, url: URL.createObjectURL(blob), bytes: copy.length, width, height }); + } else { + setLastError(`Snapshot saved, but it couldn't be downloaded to show it (${name})`); + } + } catch (e) { + console.warn('Snapshot download failed', e); + setLastError(`Snapshot saved, but it couldn't be downloaded to show it (${name})`); + } + setSnapshotProgress(null); + }; + + const handleSnapshotDownload = () => { + if (!snapshot) return; + const a = document.createElement('a'); + a.href = snapshot.url; + a.download = snapshot.name; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + }; + // Unsubscribe from the device topic on unmount if live view is on useEffect(() => { return () => { @@ -214,6 +276,10 @@ export default function CameraPanel({ onSnapshotTaken }: CameraPanelProps) { if (r?.rslt === 'ok') { setLastMsg(`Saved ${r.filename ?? ''} (${r.bytes ?? 0} bytes, ${r.width ?? 0}x${r.height ?? 0})`); onSnapshotTaken?.(); + // Show it (a device that only queued the capture gives no size yet - see it in the image files instead) + if (r.filename && r.fs && r.bytes) { + await loadSnapshot(r.fs, r.filename, r.width ?? 0, r.height ?? 0); + } } else { setLastError('Snapshot failed'); } @@ -294,6 +360,26 @@ export default function CameraPanel({ onSnapshotTaken }: CameraPanelProps) { )} + {snapshotProgress !== null && ( +
Loading snapshot... {snapshotProgress}%
+ )} + {snapshot && ( +
+ Snapshot +
+ Snapshot {snapshot.name} · {snapshot.width}x{snapshot.height} · {(snapshot.bytes / 1024).toFixed(1)} KB +
+
+ + +
+
+ )} +