diff --git a/lightsync/frontend/app.js b/lightsync/frontend/app.js index 842acdf..8e253a5 100644 --- a/lightsync/frontend/app.js +++ b/lightsync/frontend/app.js @@ -173,6 +173,77 @@ function wireAudio() { }); } +// ── Keyboard shortcuts ────────────────────────────────────────────────────── + +function wireKeyboard() { + const audio = document.getElementById('player'); + + document.addEventListener('keydown', (e) => { + // Skip shortcuts when typing in form elements + const tag = document.activeElement?.tagName?.toLowerCase(); + if (['input', 'select', 'textarea'].includes(tag)) return; + // Also skip Space on focused buttons to prevent double-toggle (Pitfall 6) + if (e.code === 'Space' && tag === 'button') return; + + if (e.code === 'Space') { + e.preventDefault(); + if (!audio) return; + if (audio.paused) { + audio.play(); + client.send({ type: 'play', position: audio.currentTime }); + } else { + audio.pause(); + client.send({ type: 'pause', position: audio.currentTime }); + } + } else if (e.code === 'ArrowLeft') { + e.preventDefault(); + if (!audio) return; + audio.currentTime = Math.max(0, audio.currentTime - 5); + client.send({ type: 'seek', position: audio.currentTime }); + // Clear preview strips on seek + document.querySelectorAll('.device-strip').forEach(s => { + s.classList.remove('active'); + const colorEl = s.querySelector('.strip-color'); + if (colorEl) colorEl.style.backgroundColor = ''; + const animEl = s.querySelector('.strip-anim'); + if (animEl) animEl.textContent = '\u2014'; + }); + } else if (e.code === 'ArrowRight') { + e.preventDefault(); + if (!audio) return; + audio.currentTime = Math.min(audio.duration || 0, audio.currentTime + 5); + client.send({ type: 'seek', position: audio.currentTime }); + // Clear preview strips on seek + document.querySelectorAll('.device-strip').forEach(s => { + s.classList.remove('active'); + const colorEl = s.querySelector('.strip-color'); + if (colorEl) colorEl.style.backgroundColor = ''; + const animEl = s.querySelector('.strip-anim'); + if (animEl) animEl.textContent = '\u2014'; + }); + } else if (e.ctrlKey && !e.shiftKey && e.code === 'KeyZ') { + e.preventDefault(); + timeline?.history.undo(); + } else if ((e.ctrlKey && e.shiftKey && e.code === 'KeyZ') || (e.ctrlKey && e.code === 'KeyY')) { + e.preventDefault(); + timeline?.history.redo(); + } else if (e.ctrlKey && e.code === 'KeyS') { + e.preventDefault(); // Prevent browser Save dialog (Pitfall 7) + if (timeline?.showId) { + fetch(`/api/shows/${timeline.showId}`) + .then(r => r.json()) + .then(show => fetch('/api/shows', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(show), + })) + .then(() => console.log('[save] show saved via Ctrl+S')) + .catch(e => console.warn('[save]', e)); + } + } + }); +} + // ── File list ──────────────────────────────────────────────────────────────── async function refreshFiles(selectPath) { @@ -277,6 +348,7 @@ document.getElementById('add-device-form')?.addEventListener('submit', async (e) // ── Init ───────────────────────────────────────────────────────────────────── wireAudio(); +wireKeyboard(); // keyboard shortcuts (Phase 5) // ── Preview strip builder ────────────────────────────────────────────────────