feat(05-03): add wireKeyboard() with all 7 keyboard shortcuts

- Space toggles play/pause, sends play/pause msg to server
- ArrowLeft/ArrowRight seek +/-5s, send seek msg, clear preview strips
- Ctrl+Z calls timeline.history.undo()
- Ctrl+Shift+Z and Ctrl+Y both call timeline.history.redo()
- Ctrl+S prevents browser dialog, fetches show and POSTs to save
- All shortcuts suppressed when focus is on input/select/textarea
- Space skips when active element is a button (prevents double-toggle)
- wireKeyboard() called at init after wireAudio()
This commit is contained in:
Claude
2026-04-07 10:18:19 +00:00
parent 1499736fde
commit 3562652f2b

View File

@@ -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 ──────────────────────────────────────────────────────────────── // ── File list ────────────────────────────────────────────────────────────────
async function refreshFiles(selectPath) { async function refreshFiles(selectPath) {
@@ -277,6 +348,7 @@ document.getElementById('add-device-form')?.addEventListener('submit', async (e)
// ── Init ───────────────────────────────────────────────────────────────────── // ── Init ─────────────────────────────────────────────────────────────────────
wireAudio(); wireAudio();
wireKeyboard(); // keyboard shortcuts (Phase 5)
// ── Preview strip builder ──────────────────────────────────────────────────── // ── Preview strip builder ────────────────────────────────────────────────────