/** * Vue composable for handling YouTube-style keyboard shortcuts in video player */ import { ref } from 'vue'; import type { Ref } from 'vue'; import { playPause, rewind, forward, mute, changeVolume, changeSpeed, frameStep, seekToPercent, seekBackward, seekForward, seekStart, seekEnd, fullscreen, pictureInPicture, toggleCaptions, shouldHandleKeyboardShortcut, modifierKey, } from '~/utils/keyboard'; import type { KeyboardShortcutContext } from '~/types/video'; export interface UseKeyboardShortcutsOptions { videoElement: Ref; enabled?: Ref; closePlayer?: () => void; onHelpToggle?: () => void; } export const useKeyboardShortcuts = (options: UseKeyboardShortcutsOptions) => { const { videoElement, enabled, closePlayer, onHelpToggle } = options; const showHelp = ref(false); const handleKeyDown = async (event: KeyboardEvent) => { // Don't handle if composable is disabled if (enabled && !enabled.value) { return; } // Don't handle if user is typing in an input if (!shouldHandleKeyboardShortcut(event)) { return; } const video = videoElement.value; if (!video) { return; } // Skip if modifier keys are pressed (except for shortcuts that need them) if (modifierKey(event) && !['f', 'p', '?'].includes(event.key.toLowerCase())) { return; } const key = event.key.toLowerCase(); const ctx: KeyboardShortcutContext = { video }; try { switch (key) { // Play/Pause case ' ': case 'k': event.preventDefault(); playPause(ctx); break; // Rewind 10 seconds (J key) case 'j': event.preventDefault(); rewind(ctx, 10); break; // Forward 10 seconds (L key) case 'l': event.preventDefault(); forward(ctx, 10); break; // Seek backward 5 seconds (left arrow) case 'arrowleft': event.preventDefault(); seekBackward(ctx, 5); break; // Seek forward 5 seconds (right arrow) case 'arrowright': event.preventDefault(); seekForward(ctx, 5); break; // Increase volume (up arrow) case 'arrowup': event.preventDefault(); changeVolume(ctx, 0.1); break; // Decrease volume (down arrow) case 'arrowdown': event.preventDefault(); changeVolume(ctx, -0.1); break; // Mute/Unmute case 'm': event.preventDefault(); mute(ctx); break; // Toggle fullscreen case 'f': event.preventDefault(); fullscreen(video); break; // Picture-in-Picture case 'p': event.preventDefault(); await pictureInPicture(video); break; // Toggle captions case 'c': event.preventDefault(); toggleCaptions(video); break; // Frame advance (period key) / Increase playback speed (> or ') case '.': case "'": { event.preventDefault(); if ('.' === key) { frameStep(ctx, 'forward'); } else { changeSpeed(ctx, 0.25); } break; } // Frame rewind (comma key) / Decrease playback speed (< or ;) case ',': case ';': { event.preventDefault(); if (',' === key) { frameStep(ctx, 'backward'); } else { changeSpeed(ctx, -0.25); } break; } // Jump to percentage (0-9 keys) case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': { event.preventDefault(); const percent = parseInt(key) * 10; seekToPercent(ctx, percent); break; } // Jump to start (Home) case 'home': event.preventDefault(); seekStart(ctx); break; // Jump to end (End) case 'end': event.preventDefault(); seekEnd(ctx); break; // Show/hide help (Shift+/) case '?': case '/': { event.preventDefault(); showHelp.value = !showHelp.value; onHelpToggle?.(); break; } // Close player (Escape - already handled elsewhere but kept for completeness) case 'escape': event.preventDefault(); closePlayer?.(); break; default: // No action for unrecognized keys break; } } catch (error) { console.error('Error handling keyboard shortcut:', error); } }; const attach = () => { document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }; return { showHelp, attach, }; };