218 lines
5 KiB
TypeScript
218 lines
5 KiB
TypeScript
/**
|
|
* 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<HTMLVideoElement | null | undefined>;
|
|
enabled?: Ref<boolean>;
|
|
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,
|
|
};
|
|
};
|