feat(web): desktop keyboard shortcuts
Space = play/pause, ←/→ = seek 5s, Shift+←/→ = prev/next, ↑/↓ = volume. Handler lives in player.js (where _pos/_dur/seekToMs are in scope), wired from startApp via initKeyboardShortcuts(). Ignored while typing in inputs, and yields Space to focused buttons/links so it never double-fires. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
ffa4fb35ea
commit
7498e45b16
3 files changed
+53
-1
No files matched your search
@@ -898,6 +898,56 @@ export function seekToMs(ms) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── DESKTOP KEYBOARD SHORTCUTS ───
|
||||
// Space = play/pause · ←/→ = seek 5s · Shift+←/→ = prev/next · ↑/↓ = volume.
|
||||
// Skipped while typing in an input so it never hijacks the search/playlist fields.
|
||||
export function initKeyboardShortcuts() {
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey || e.altKey || e.metaKey) return; // leave OS/browser combos alone
|
||||
const t = e.target;
|
||||
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' ||
|
||||
t.tagName === 'SELECT' || t.isContentEditable)) return;
|
||||
|
||||
switch (e.code) {
|
||||
case 'Space':
|
||||
// Don't hijack Space when a button/link is focused — it would double-fire.
|
||||
if (t && (t.tagName === 'BUTTON' || t.tagName === 'A' ||
|
||||
t.closest?.('button, a, [role="button"]'))) return;
|
||||
e.preventDefault();
|
||||
togglePlay();
|
||||
break;
|
||||
case 'ArrowRight':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) nextSong(); else _seekBy(5);
|
||||
break;
|
||||
case 'ArrowLeft':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) prevSong(); else _seekBy(-5);
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
_bumpVolume(5);
|
||||
break;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
_bumpVolume(-5);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function _seekBy(deltaSec) {
|
||||
const dur = _dur();
|
||||
if (!dur) return;
|
||||
seekToMs(Math.max(0, Math.min(dur, _pos() + deltaSec)) * 1000);
|
||||
}
|
||||
|
||||
function _bumpVolume(deltaSlider) {
|
||||
// setVolume takes a 0–100 slider value (squared internally); recover it from the actual volume.
|
||||
const cur = Math.round(Math.sqrt(Math.max(0, S.audio.volume || 0)) * 100);
|
||||
setVolume(Math.max(0, Math.min(100, cur + deltaSlider)));
|
||||
}
|
||||
|
||||
function _initBarScrub(bar, fill, onMove, noTouch = false) {
|
||||
if(!bar || !fill) return;
|
||||
function getPos(clientX) { const r = bar.getBoundingClientRect(); return Math.max(0, Math.min(1, (clientX-r.left)/r.width)); }
|
||||
|
||||
Reference in new issue
Block a user