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:
@@ -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 ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user