[FEAT] Add keyboard shortcuts to built-in video player
This commit is contained in:
parent
44fd225ab1
commit
4b996a5675
5 changed files with 576 additions and 7 deletions
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
|
|
@ -18,7 +18,11 @@
|
|||
"aiocron",
|
||||
"anyio",
|
||||
"Archiver",
|
||||
"arrowdown",
|
||||
"arrowleft",
|
||||
"arrowless",
|
||||
"arrowright",
|
||||
"arrowup",
|
||||
"asyncio",
|
||||
"attl",
|
||||
"autonumber",
|
||||
|
|
@ -36,6 +40,7 @@
|
|||
"cifs",
|
||||
"connectionpool",
|
||||
"consoletitle",
|
||||
"contenteditable",
|
||||
"continuedl",
|
||||
"cookiesfrombrowser",
|
||||
"copyts",
|
||||
|
|
|
|||
|
|
@ -6,18 +6,230 @@
|
|||
height: auto;
|
||||
max-height: 80vh;
|
||||
max-width: 80vw;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.keyboard-help {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
color: var(--bulma-white);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
border-radius: 4px;
|
||||
overflow-y: auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.shortcuts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 2rem;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.shortcut-section {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.shortcut-section h3 {
|
||||
color: var(--bulma-primary);
|
||||
margin-bottom: 1rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.shortcut-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 0.75rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.shortcut-item span:first-child {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.shortcut-item .kbd-group {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.shortcut-key {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 3px;
|
||||
margin-right: 1rem;
|
||||
font-family: monospace;
|
||||
font-weight: bold;
|
||||
min-width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.help-close-hint {
|
||||
margin-top: 2rem;
|
||||
font-size: 0.9rem;
|
||||
color: var(--bulma-light);
|
||||
}
|
||||
</style>
|
||||
|
||||
<template>
|
||||
<div v-if="infoLoaded">
|
||||
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls
|
||||
crossorigin="anonymous" preload="auto" autoplay>
|
||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||
:type="source.type" />
|
||||
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
||||
:srclang="track.lang" :src="track.file" :default="notFirefox && 0 === i" />
|
||||
</video>
|
||||
<div style="position: relative;">
|
||||
<video class="player" ref="video" :poster="uri(thumbnail)" :title="title" playsinline controls
|
||||
crossorigin="anonymous" preload="auto" autoplay>
|
||||
<source v-for="source in sources" :key="source.src" :src="source.src" @error="source.onerror"
|
||||
:type="source.type" />
|
||||
<track v-for="(track, i) in tracks" :key="track.file" :kind="track.kind" :label="track.label"
|
||||
:srclang="track.lang" :src="track.file" :default="notFirefox && 0 === i" />
|
||||
</video>
|
||||
|
||||
<span class="has-text-white is-pointer" @click="showHelp = !showHelp" title="Keyboard shortcuts (or press ?)">
|
||||
<span class="icon"><i class="fa-solid fa-question" /></span>
|
||||
<span>Click here to Keyboard shortcuts or press <kbd>?</kbd> or <kbd>/</kbd></span>
|
||||
</span>
|
||||
|
||||
<div v-if="showHelp" class="keyboard-help" @click.self="showHelp = false">
|
||||
<h2 style="margin-bottom: 1.5rem;">Keyboard Shortcuts</h2>
|
||||
|
||||
<div class="shortcuts-grid">
|
||||
<div class="shortcut-section">
|
||||
<h3>Playback Control</h3>
|
||||
<div class="shortcut-item">
|
||||
<span>Play/Pause</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">SPACE</kbd>
|
||||
<kbd class="shortcut-key">K</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Rewind 10s</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">J</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Forward 10s</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">L</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Mute/Unmute</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">M</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shortcut-section">
|
||||
<h3>Seeking</h3>
|
||||
<div class="shortcut-item">
|
||||
<span>Seek Back 5s</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">←</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Seek Forward 5s</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">→</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Jump to Start</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">HOME</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Jump to End</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">END</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Jump to %</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">0-9</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shortcut-section">
|
||||
<h3>Volume & Speed</h3>
|
||||
<div class="shortcut-item">
|
||||
<span>Increase Volume</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">↑</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Decrease Volume</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">↓</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Increase Speed</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">'</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Decrease Speed</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">;</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="shortcut-section">
|
||||
<h3>Display & Other</h3>
|
||||
<div class="shortcut-item">
|
||||
<span>Fullscreen</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">F</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Picture in Picture (PiP)</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">P</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Toggle Captions</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">C</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Frame Advance</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">.</kbd>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shortcut-item">
|
||||
<span>Frame Rewind</span>
|
||||
<div class="kbd-group">
|
||||
<kbd class="shortcut-key">,</kbd>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="help-close-hint">Click outside or press <kbd>?</kbd> or <kbd>/</kbd> to close</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="text-align: center;" v-else>
|
||||
<div class="icon">
|
||||
|
|
@ -28,8 +240,10 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { watch } from 'vue'
|
||||
import Hls from 'hls.js'
|
||||
import { disableOpacity, enableOpacity } from '~/utils'
|
||||
import { useKeyboardShortcuts } from '~/composables/useKeyboardShortcuts'
|
||||
|
||||
import type { StoreItem } from '~/types/store'
|
||||
import type { file_info, video_source_element, video_track_element } from '~/types/video'
|
||||
|
|
@ -61,10 +275,12 @@ const infoLoaded = ref(false)
|
|||
const destroyed = ref(false)
|
||||
const isApple = /(iPhone|iPod|iPad).*AppleWebKit/i.test(navigator.userAgent)
|
||||
const havePoster = ref(false)
|
||||
const showHelp = ref(false)
|
||||
|
||||
let unbindMediaSessionListeners: null | (() => void) = null
|
||||
let hls: Hls | null = null
|
||||
let detachDecodeGuard: null | (() => void) = null
|
||||
let cleanupKeyboardShortcuts: null | (() => void) = null
|
||||
|
||||
const handle_event = (e: KeyboardEvent) => {
|
||||
if ('Escape' !== e.key) {
|
||||
|
|
@ -296,6 +512,24 @@ onMounted(async () => {
|
|||
unbindMediaSessionListeners = bindMediaSessionListeners(video.value)
|
||||
}
|
||||
|
||||
// Initialize keyboard shortcuts
|
||||
const keyboardShortcutsResult = useKeyboardShortcuts({
|
||||
videoElement: video,
|
||||
enabled: ref(true),
|
||||
closePlayer: () => emitter('closeModel'),
|
||||
onHelpToggle: () => {
|
||||
// Help toggle callback (optional)
|
||||
}
|
||||
})
|
||||
|
||||
// Attach the keyboard shortcuts listener and store cleanup function
|
||||
cleanupKeyboardShortcuts = keyboardShortcutsResult.attach()
|
||||
|
||||
// Bind the showHelp from the composable
|
||||
watch(keyboardShortcutsResult.showHelp, (newVal) => {
|
||||
showHelp.value = newVal
|
||||
})
|
||||
|
||||
document.addEventListener('keydown', handle_event)
|
||||
})
|
||||
|
||||
|
|
@ -323,6 +557,11 @@ onBeforeUnmount(() => {
|
|||
|
||||
document.removeEventListener('keydown', handle_event)
|
||||
|
||||
if (cleanupKeyboardShortcuts) {
|
||||
cleanupKeyboardShortcuts()
|
||||
cleanupKeyboardShortcuts = null
|
||||
}
|
||||
|
||||
if (unbindMediaSessionListeners) {
|
||||
unbindMediaSessionListeners()
|
||||
unbindMediaSessionListeners = null
|
||||
|
|
|
|||
217
ui/app/composables/useKeyboardShortcuts.ts
Normal file
217
ui/app/composables/useKeyboardShortcuts.ts
Normal file
|
|
@ -0,0 +1,217 @@
|
|||
/**
|
||||
* Vue composable for handling YouTube-style keyboard shortcuts in video player
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import {
|
||||
handlePlayPause,
|
||||
handleRewind,
|
||||
handleForward,
|
||||
handleMute,
|
||||
handleVolumeChange,
|
||||
handlePlaybackSpeedChange,
|
||||
handleFrameStep,
|
||||
handleSeekToPercent,
|
||||
handleSeekBackward,
|
||||
handleSeekForward,
|
||||
handleFullscreen,
|
||||
handlePictureInPicture,
|
||||
handleToggleCaptions,
|
||||
shouldHandleKeyboardShortcut,
|
||||
isModifierKey,
|
||||
} 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 (isModifierKey(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()
|
||||
handlePlayPause(ctx)
|
||||
break
|
||||
|
||||
// Rewind 10 seconds (J key)
|
||||
case 'j':
|
||||
event.preventDefault()
|
||||
handleRewind(ctx, 10)
|
||||
break
|
||||
|
||||
// Forward 10 seconds (L key)
|
||||
case 'l':
|
||||
event.preventDefault()
|
||||
handleForward(ctx, 10)
|
||||
break
|
||||
|
||||
// Seek backward 5 seconds (left arrow)
|
||||
case 'arrowleft':
|
||||
event.preventDefault()
|
||||
handleSeekBackward(ctx, 5)
|
||||
break
|
||||
|
||||
// Seek forward 5 seconds (right arrow)
|
||||
case 'arrowright':
|
||||
event.preventDefault()
|
||||
handleSeekForward(ctx, 5)
|
||||
break
|
||||
|
||||
// Increase volume (up arrow)
|
||||
case 'arrowup':
|
||||
event.preventDefault()
|
||||
handleVolumeChange(ctx, 0.1)
|
||||
break
|
||||
|
||||
// Decrease volume (down arrow)
|
||||
case 'arrowdown':
|
||||
event.preventDefault()
|
||||
handleVolumeChange(ctx, -0.1)
|
||||
break
|
||||
|
||||
// Mute/Unmute
|
||||
case 'm':
|
||||
event.preventDefault()
|
||||
handleMute(ctx)
|
||||
break
|
||||
|
||||
// Toggle fullscreen
|
||||
case 'f':
|
||||
event.preventDefault()
|
||||
handleFullscreen(video)
|
||||
break
|
||||
|
||||
// Picture-in-Picture
|
||||
case 'p':
|
||||
event.preventDefault()
|
||||
await handlePictureInPicture(video)
|
||||
break
|
||||
|
||||
// Toggle captions
|
||||
case 'c':
|
||||
event.preventDefault()
|
||||
handleToggleCaptions(video)
|
||||
break
|
||||
|
||||
// Frame advance (period key) / Increase playback speed (> or ')
|
||||
case '.':
|
||||
case "'": {
|
||||
event.preventDefault()
|
||||
if ('.' === key) {
|
||||
handleFrameStep(ctx, 'forward')
|
||||
} else {
|
||||
handlePlaybackSpeedChange(ctx, 0.25)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Frame rewind (comma key) / Decrease playback speed (< or ;)
|
||||
case ',':
|
||||
case ';': {
|
||||
event.preventDefault()
|
||||
if (',' === key) {
|
||||
handleFrameStep(ctx, 'backward')
|
||||
} else {
|
||||
handlePlaybackSpeedChange(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
|
||||
handleSeekToPercent(ctx, percent)
|
||||
break
|
||||
}
|
||||
|
||||
// Jump to start (Home)
|
||||
case 'home':
|
||||
event.preventDefault()
|
||||
video.currentTime = 0
|
||||
break
|
||||
|
||||
// Jump to end (End)
|
||||
case 'end':
|
||||
event.preventDefault()
|
||||
video.currentTime = video.duration
|
||||
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,
|
||||
}
|
||||
}
|
||||
4
ui/app/types/video.d.ts
vendored
4
ui/app/types/video.d.ts
vendored
|
|
@ -1,3 +1,6 @@
|
|||
type KeyboardShortcutContext = {
|
||||
video: HTMLVideoElement
|
||||
}
|
||||
|
||||
type video_track_element = {
|
||||
file: string,
|
||||
|
|
@ -82,4 +85,5 @@ export type {
|
|||
video_source_element,
|
||||
FFProbeResult,
|
||||
file_info,
|
||||
KeyboardShortcutContext,
|
||||
};
|
||||
|
|
|
|||
104
ui/app/utils/keyboard.ts
Normal file
104
ui/app/utils/keyboard.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import type { KeyboardShortcutContext } from '~/types/video'
|
||||
|
||||
export const handlePlayPause = (ctx: KeyboardShortcutContext) => {
|
||||
if (ctx.video.paused) {
|
||||
ctx.video.play()
|
||||
} else {
|
||||
ctx.video.pause()
|
||||
}
|
||||
}
|
||||
|
||||
export const handleRewind = (ctx: KeyboardShortcutContext, seconds: number = 10) => {
|
||||
ctx.video.currentTime = Math.max(0, ctx.video.currentTime - seconds)
|
||||
}
|
||||
|
||||
export const handleForward = (ctx: KeyboardShortcutContext, seconds: number = 10) => {
|
||||
ctx.video.currentTime = Math.min(ctx.video.duration, ctx.video.currentTime + seconds)
|
||||
}
|
||||
|
||||
export const handleMute = (ctx: KeyboardShortcutContext) => {
|
||||
ctx.video.muted = !ctx.video.muted
|
||||
}
|
||||
|
||||
export const handleVolumeChange = (ctx: KeyboardShortcutContext, delta: number) => {
|
||||
const newVolume = Math.max(0, Math.min(1, ctx.video.volume + delta))
|
||||
ctx.video.volume = newVolume
|
||||
}
|
||||
|
||||
export const handlePlaybackSpeedChange = (ctx: KeyboardShortcutContext, delta: number) => {
|
||||
const currentSpeed = ctx.video.playbackRate
|
||||
const newSpeed = Math.max(0.25, Math.min(2, currentSpeed + delta))
|
||||
ctx.video.playbackRate = newSpeed
|
||||
}
|
||||
|
||||
export const handleFrameStep = (ctx: KeyboardShortcutContext, direction: 'forward' | 'backward' = 'forward') => {
|
||||
if (!ctx.video.paused) {
|
||||
ctx.video.pause()
|
||||
}
|
||||
|
||||
// Frame step by ~33ms (approximately 1 frame at 30fps, ~16.7ms at 60fps)
|
||||
const frameStep = 'forward' === direction ? 0.033 : -0.033
|
||||
ctx.video.currentTime = Math.max(0, Math.min(ctx.video.duration, ctx.video.currentTime + frameStep))
|
||||
}
|
||||
|
||||
export const handleSeekToPercent = (ctx: KeyboardShortcutContext, percent: number) => {
|
||||
ctx.video.currentTime = (percent / 100) * ctx.video.duration
|
||||
}
|
||||
|
||||
export const handleSeekBackward = (ctx: KeyboardShortcutContext, seconds: number = 5) => {
|
||||
ctx.video.currentTime = Math.max(0, ctx.video.currentTime - seconds)
|
||||
}
|
||||
|
||||
export const handleSeekForward = (ctx: KeyboardShortcutContext, seconds: number = 5) => {
|
||||
ctx.video.currentTime = Math.min(ctx.video.duration, ctx.video.currentTime + seconds)
|
||||
}
|
||||
|
||||
export const handleFullscreen = (videoElement: HTMLVideoElement) => {
|
||||
if (!document.fullscreenElement) {
|
||||
videoElement.requestFullscreen().catch(err => {
|
||||
console.error(`Error attempting to enable fullscreen: ${err.message}`)
|
||||
})
|
||||
} else {
|
||||
document.exitFullscreen()
|
||||
}
|
||||
}
|
||||
|
||||
export const handlePictureInPicture = async (videoElement: HTMLVideoElement) => {
|
||||
try {
|
||||
if (document.pictureInPictureElement) {
|
||||
await document.exitPictureInPicture()
|
||||
} else if (document.pictureInPictureEnabled) {
|
||||
await videoElement.requestPictureInPicture()
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Picture-in-Picture error: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
export const handleToggleCaptions = (videoElement: HTMLVideoElement) => {
|
||||
const textTracks = videoElement.textTracks
|
||||
|
||||
if (0 === textTracks.length) {
|
||||
return
|
||||
}
|
||||
|
||||
for (let i = 0; i < textTracks.length; i++) {
|
||||
const track = textTracks[i] as TextTrack | null
|
||||
if (track && ('captions' === track.kind || 'subtitles' === track.kind)) {
|
||||
track.mode = 'showing' === track.mode ? 'hidden' : 'showing'
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldHandleKeyboardShortcut = (event: KeyboardEvent): boolean => {
|
||||
const target = event.target as HTMLElement
|
||||
const tagName = target?.tagName?.toLowerCase()
|
||||
if ('input' === tagName || 'textarea' === tagName || 'true' === target?.contentEditable || 'true' === target?.getAttribute('contenteditable')) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const isModifierKey = (event: KeyboardEvent): boolean => event.ctrlKey || event.metaKey || event.altKey
|
||||
Loading…
Reference in a new issue