function SnapshotPanel({ onClose, state, connected }) { const [snapshots, setSnapshots] = useState({}); const [loading, setLoading] = useState(true); const [editingSlot, setEditingSlot] = useState(null); const [editName, setEditName] = useState(''); const [saveFlash, setSaveFlash] = useState(null); const [discardEdits, setDiscardEdits] = useState(false); const holdTimer = useRef(null); const currentSlot = state?.current_snapshot ?? 0; const loadSnapshots = useCallback(async () => { if (!connected) { setLoading(false); return; } try { const data = await apiGet('/api/snapshots'); setSnapshots(data.snapshots || {}); } catch (e) { /* ignore */ } setLoading(false); }, [connected]); useEffect(() => { loadSnapshots(); }, [loadSnapshots]); // Handle press & hold vs tap const handlePointerDown = (slot) => { holdTimer.current = setTimeout(() => { // Long press → save holdTimer.current = null; handleSave(slot); }, 600); }; const handlePointerUp = (slot) => { if (holdTimer.current) { clearTimeout(holdTimer.current); holdTimer.current = null; // Short tap → recall (or save if empty) if (snapshots[String(slot)]) { handleRecall(slot); } else { handleSave(slot); } } }; const handlePointerLeave = () => { if (holdTimer.current) { clearTimeout(holdTimer.current); holdTimer.current = null; } }; const handleSave = async (slot) => { try { await apiPost(`/api/snapshots/${slot}/save`); setSaveFlash(slot); setTimeout(() => setSaveFlash(null), 800); await loadSnapshots(); } catch (e) { /* ignore */ } }; const handleRecall = async (slot) => { try { await apiPost(`/api/snapshots/${slot}/recall`); await loadSnapshots(); } catch (e) { /* ignore */ } }; const handleRename = async (slot) => { if (!editName.trim()) return; try { await apiPut(`/api/snapshots/${slot}`, { name: editName.trim() }); setEditingSlot(null); await loadSnapshots(); } catch (e) { /* ignore */ } }; const handleDelete = async (slot) => { try { await apiDelete(`/api/snapshots/${slot}`); await loadSnapshots(); } catch (e) { /* ignore */ } }; return (
Snapshots
Tap to recall · Hold to save {discardEdits && Edits will be discarded on recall}
{[1,2,3,4,5,6,7,8].map(slot => { const snap = snapshots[String(slot)]; const isActive = currentSlot === slot; const isFilled = !!snap; const isEditing = editingSlot === slot; const isFlashing = saveFlash === slot; return (
handlePointerDown(slot)} onTouchStart={() => handlePointerDown(slot)} onMouseUp={() => handlePointerUp(slot)} onTouchEnd={() => handlePointerUp(slot)} onMouseLeave={handlePointerLeave} onTouchCancel={handlePointerLeave} style={isFlashing ? { background: 'rgba(58,184,122,.15)' } : {}}>
{slot}
{isEditing ? ( setEditName(e.target.value)} onBlur={() => handleRename(slot)} onKeyDown={e => { if (e.key === 'Enter') handleRename(slot); if (e.key === 'Escape') setEditingSlot(null); }} autoFocus onClick={e => e.stopPropagation()} /> ) : snap ? (
{ e.stopPropagation(); setEditingSlot(slot); setEditName(snap.name); }}>{snap.name}
) : (
Empty
)} {isFlashing &&
} {isActive &&
}
); })}
{/* Delete button for active snapshot */} {currentSlot > 0 && snapshots[String(currentSlot)] && (
)}
); }