Initial release v1.0.0
This commit is contained in:
139
frontend/hooks/useKeyboardShortcuts.ts
Normal file
139
frontend/hooks/useKeyboardShortcuts.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useAudio } from '@/lib/audio-context';
|
||||
import { useIsTV } from '@/lib/tv-utils';
|
||||
|
||||
/**
|
||||
* Global keyboard shortcuts for media playback
|
||||
*
|
||||
* Shortcuts:
|
||||
* - Space: Play/Pause
|
||||
* - Arrow Right: Seek forward 10s
|
||||
* - Arrow Left: Seek backward 10s
|
||||
* - Arrow Up: Volume up 10%
|
||||
* - Arrow Down: Volume down 10%
|
||||
* - M: Toggle mute
|
||||
* - N: Next track
|
||||
* - P: Previous track
|
||||
* - S: Toggle shuffle
|
||||
*/
|
||||
export function useKeyboardShortcuts() {
|
||||
const isTV = useIsTV();
|
||||
const {
|
||||
isPlaying,
|
||||
resume,
|
||||
pause,
|
||||
next,
|
||||
previous,
|
||||
seek,
|
||||
currentTime,
|
||||
setVolume,
|
||||
volume,
|
||||
toggleMute,
|
||||
toggleShuffle,
|
||||
playbackType,
|
||||
currentTrack,
|
||||
currentAudiobook,
|
||||
currentPodcast,
|
||||
} = useAudio();
|
||||
|
||||
useEffect(() => {
|
||||
// Disable keyboard shortcuts on TV - use remote's media keys instead
|
||||
if (isTV) return;
|
||||
|
||||
// Don't add shortcuts if nothing is loaded
|
||||
if (!playbackType) return;
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
// Ignore if user is typing in an input/textarea
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.tagName === 'INPUT' ||
|
||||
target.tagName === 'TEXTAREA' ||
|
||||
target.isContentEditable
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent default for media keys to avoid conflicts
|
||||
if ([' ', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'].includes(e.key)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
switch (e.key.toLowerCase()) {
|
||||
case ' ': // Space - Play/Pause
|
||||
if (isPlaying) {
|
||||
pause();
|
||||
} else {
|
||||
resume();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'arrowright': // Right arrow - Seek forward 10s
|
||||
if (playbackType === 'track' || playbackType === 'audiobook' || playbackType === 'podcast') {
|
||||
const duration = currentTrack?.duration || currentAudiobook?.duration || currentPodcast?.duration || 0;
|
||||
seek(Math.min(currentTime + 10, duration));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'arrowleft': // Left arrow - Seek backward 10s
|
||||
if (playbackType === 'track' || playbackType === 'audiobook' || playbackType === 'podcast') {
|
||||
seek(Math.max(currentTime - 10, 0));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'arrowup': // Up arrow - Volume up 10%
|
||||
setVolume(Math.min(volume + 0.1, 1));
|
||||
break;
|
||||
|
||||
case 'arrowdown': // Down arrow - Volume down 10%
|
||||
setVolume(Math.max(volume - 0.1, 0));
|
||||
break;
|
||||
|
||||
case 'm': // M - Toggle mute
|
||||
toggleMute();
|
||||
break;
|
||||
|
||||
case 'n': // N - Next track
|
||||
if (playbackType === 'track') {
|
||||
next();
|
||||
}
|
||||
break;
|
||||
|
||||
case 'p': // P - Previous track
|
||||
if (playbackType === 'track' && !e.shiftKey) { // Avoid conflict with Shift+P
|
||||
previous();
|
||||
}
|
||||
break;
|
||||
|
||||
case 's': // S - Toggle shuffle
|
||||
if (playbackType === 'track') {
|
||||
toggleShuffle();
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyPress);
|
||||
return () => window.removeEventListener('keydown', handleKeyPress);
|
||||
}, [
|
||||
isTV,
|
||||
isPlaying,
|
||||
pause,
|
||||
resume,
|
||||
next,
|
||||
previous,
|
||||
seek,
|
||||
currentTime,
|
||||
setVolume,
|
||||
volume,
|
||||
toggleMute,
|
||||
toggleShuffle,
|
||||
playbackType,
|
||||
currentTrack,
|
||||
currentAudiobook,
|
||||
currentPodcast,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user