mirror of
https://github.com/the1812/Bilibili-Evolved.git
synced 2025-11-04 21:22:45 +08:00
125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
import { getActiveElement, isTyping, matchUrlPattern } from '@/core/utils'
|
|
import { mediaListUrls, watchlaterUrls } from '@/core/utils/urls'
|
|
import { clickElement, changeVideoTime, showTip } from './actions'
|
|
import { shadowDomObserver } from '@/core/shadow-root'
|
|
|
|
export interface KeyBindingActionContext {
|
|
binding: KeyBinding
|
|
event: KeyboardEvent
|
|
isWatchlater: boolean
|
|
isMediaList: boolean
|
|
clickElement: typeof clickElement
|
|
changeVideoTime: typeof changeVideoTime
|
|
showTip: typeof showTip
|
|
}
|
|
export interface KeyBindingAction {
|
|
displayName: string
|
|
run: (context: KeyBindingActionContext) => unknown
|
|
prevent?: boolean
|
|
ignoreTyping?: boolean
|
|
ignoreFocus?: boolean
|
|
}
|
|
export interface KeyBinding {
|
|
keys: string[]
|
|
action: KeyBindingAction
|
|
}
|
|
const modifyKeys = ['shift', 'alt', 'ctrl', 'meta']
|
|
export const loadKeyBindings = lodash.once((bindings: KeyBinding[]) => {
|
|
const isWatchlater = watchlaterUrls.some(url => matchUrlPattern(url))
|
|
const isMediaList = mediaListUrls.some(url => matchUrlPattern(url))
|
|
const config = {
|
|
enable: true,
|
|
bindings,
|
|
}
|
|
const keyboardHandler = (e: KeyboardEvent & { [key: string]: boolean }) => {
|
|
if (!config.enable) {
|
|
return
|
|
}
|
|
config.bindings.forEach(binding => {
|
|
if (binding.keys.length === 0) {
|
|
return
|
|
}
|
|
|
|
// 打字时无视快捷键
|
|
if (binding.action.ignoreTyping !== false && isTyping()) {
|
|
return
|
|
}
|
|
|
|
// 忽略其他可聚焦元素
|
|
const hasElementFocus = (() => {
|
|
const activeElement = getActiveElement()
|
|
if (([document.body, null] as (Element | null)[]).includes(activeElement)) {
|
|
return false
|
|
}
|
|
if (activeElement instanceof HTMLMediaElement) {
|
|
return false
|
|
}
|
|
return true
|
|
})()
|
|
if (
|
|
binding.action.ignoreFocus !== false &&
|
|
binding.action.ignoreTyping !== false &&
|
|
hasElementFocus
|
|
) {
|
|
return
|
|
}
|
|
|
|
const key = e.key.toLowerCase()
|
|
|
|
// 全景视频禁用 WASD 快捷键
|
|
const panoramaControl = dq('.bilibili-player-sphere-control') as HTMLElement
|
|
if (
|
|
panoramaControl !== null &&
|
|
panoramaControl.style.display !== 'none' &&
|
|
['w', 'a', 's', 'd'].includes(key)
|
|
) {
|
|
return
|
|
}
|
|
|
|
const modifyKeyNotMatch = modifyKeys.some(m => {
|
|
const needModifyKey = binding.keys.includes(m)
|
|
const optionalModifyKey = binding.keys.includes(`[${m}]`)
|
|
if (optionalModifyKey) {
|
|
return false
|
|
}
|
|
const isModifyKeyPressed = e[`${m}Key`]
|
|
return needModifyKey !== isModifyKeyPressed
|
|
})
|
|
if (modifyKeyNotMatch) {
|
|
return
|
|
}
|
|
const restKeys = binding.keys
|
|
.filter(k => !modifyKeys.includes(k.toLowerCase()))
|
|
.map(k => k.toLowerCase())
|
|
const keyMatch =
|
|
restKeys.includes(e.key.toLowerCase()) || restKeys.includes(e.code.toLowerCase())
|
|
if (!keyMatch) {
|
|
return
|
|
}
|
|
|
|
const actionResult = binding.action.run({
|
|
binding,
|
|
isWatchlater,
|
|
isMediaList,
|
|
event: e,
|
|
clickElement,
|
|
changeVideoTime,
|
|
showTip,
|
|
})
|
|
|
|
const actionSuccess = !lodash.isNil(actionResult)
|
|
if (binding.action.prevent ?? actionSuccess) {
|
|
e.stopImmediatePropagation()
|
|
e.preventDefault()
|
|
}
|
|
})
|
|
}
|
|
document.body.addEventListener('keydown', keyboardHandler, { capture: true })
|
|
shadowDomObserver.watchShadowDom({
|
|
added: shadowDom =>
|
|
shadowDom.shadowRoot.addEventListener('keydown', keyboardHandler, { capture: true }),
|
|
})
|
|
return config
|
|
})
|
|
export type KeyBindingConfig = ReturnType<typeof loadKeyBindings>
|