import { getComponentSettings } from '@/core/settings'
import { raiseEvent } from '@/core/utils'
import { registerAndGetData } from '@/plugins/data'
import { KeyBindingAction, KeyBindingActionContext } from './bindings'
export const clickElement = (target: string | HTMLElement, context: KeyBindingActionContext) => {
const { event } = context
const mouseEvent = new MouseEvent('click', {
..._.pick(event, 'ctrlKey', 'shiftKey', 'altKey', 'metaKey'),
})
if (typeof target === 'string') {
(dq(target) as HTMLElement)?.dispatchEvent(mouseEvent)
} else {
target.dispatchEvent(mouseEvent)
}
}
export const useClickElement = (target: string | HTMLElement) => (
(context: KeyBindingActionContext) => clickElement(target, context)
)
export const changeVideoTime = (delta: number | (() => number)) => () => {
const video = dq('.bilibili-player-video video') as HTMLVideoElement
const deltaNumber = typeof delta === 'function' ? delta() : delta
console.log(`[keymap] requested video time change, delta = ${delta}`)
if (!video) {
console.log('[keymap] video element not found')
return
}
if (!unsafeWindow.player) {
// fallback
console.log('[keymap] fallback')
video.currentTime += deltaNumber
}
console.log('[keymap] player API seek')
unsafeWindow.player.seek(video.currentTime + deltaNumber, video.paused)
}
/** 提示框用的`setTimeout`句柄 */
let tipTimeoutHandle: number
/**
* 显示提示框
* @param text 文字 (可以 HTML)
* @param icon MDI 图标 class
*/
export const showTip = (text: string, icon: string) => {
let tip = dq('.keymap-tip') as HTMLDivElement
if (!tip) {
const player = dq('.bilibili-player-video-wrap')
if (!player) {
return
}
player.insertAdjacentHTML('afterbegin', /* html */`
`)
tip = dq('.keymap-tip') as HTMLDivElement
}
tip.innerHTML = text
const container = dq('.keymap-tip-container') as HTMLDivElement
const iconElement = dq(container, '.mdi') as HTMLElement
iconElement.classList.remove(...iconElement.classList.values())
iconElement.classList.add('mdi', icon)
if (tipTimeoutHandle) {
clearTimeout(tipTimeoutHandle)
}
container.classList.add('show')
tipTimeoutHandle = window.setTimeout(() => {
container.classList.remove('show')
}, 2000)
}
export const builtInActions: Record = {
fullscreen: {
displayName: '全屏',
run: useClickElement('.bilibili-player-video-btn-fullscreen'),
},
webFullscreen: {
displayName: '网页全屏',
run: useClickElement('.bilibili-player-video-web-fullscreen'),
},
wideScreen: {
displayName: '宽屏',
run: useClickElement('.bilibili-player-video-btn-widescreen'),
},
volumeUp: {
displayName: '增加音量',
run: () => {
const current = unsafeWindow.player.volume()
unsafeWindow.player.volume(current + 0.1)
showTip(`${Math.round(unsafeWindow.player.volume() * 100)}%`, 'mdi-volume-high')
},
},
volumeDown: {
displayName: '降低音量',
run: () => {
const current = unsafeWindow.player.volume()
unsafeWindow.player.volume(current - 0.1)
const after = Math.round(unsafeWindow.player.volume() * 100)
if (after === 0) {
showTip('静音', 'mdi-volume-off')
} else {
showTip(`${after}%`, 'mdi-volume-high')
}
},
},
mute: {
displayName: '静音',
run: context => {
clickElement('.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume', context)
const isMute = unsafeWindow.player.isMute()
if (isMute) {
showTip('已静音', 'mdi-volume-off')
} else {
showTip('已取消静音', 'mdi-volume-high')
}
},
},
pictureInPicture: {
displayName: '画中画',
run: useClickElement('.bilibili-player-video-btn-pip'),
},
coin: {
displayName: '投币',
run: useClickElement('.video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)'),
},
favorite: {
displayName: '收藏',
run: useClickElement('.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)'),
},
pause: {
displayName: '暂停/播放',
run: useClickElement('.bilibili-player-video-btn-start'),
},
like: {
displayName: '点赞',
run: (() => {
/** 长按`L`三连使用的记忆变量 */
let likeClick = true
return ((context: KeyBindingActionContext) => {
const { event } = context
const likeButton = dq('.video-toolbar .like, .tool-bar .like-info') as HTMLSpanElement
event.preventDefault()
const fireEvent = (name: string, args: Event) => {
const customEvent = new CustomEvent(name, args)
likeButton.dispatchEvent(customEvent)
}
likeClick = true
setTimeout(() => (likeClick = false), 200)
fireEvent('mousedown', event)
document.body.addEventListener('keyup', e => {
e.preventDefault()
fireEvent('mouseup', e)
if (likeClick) {
fireEvent('click', e)
}
}, { once: true })
})
})(),
},
danmaku: {
displayName: '弹幕开关',
run: () => {
const checkbox = dq('.bilibili-player-video-danmaku-switch input') as HTMLInputElement
if (!checkbox) {
return
}
checkbox.checked = !checkbox.checked
raiseEvent(checkbox, 'change')
},
},
longJumpBackward: {
displayName: '长倒退',
run: changeVideoTime(
() => -(getComponentSettings('keymap').options.longJumpSeconds),
),
},
longJumpForward: {
displayName: '长前进',
run: changeVideoTime(
() => getComponentSettings('keymap').options.longJumpSeconds,
),
},
jumpBackward: {
displayName: '倒退',
run: changeVideoTime(-5),
},
jumpForward: {
displayName: '前进',
run: changeVideoTime(5),
},
playerMenu: {
displayName: '播放器菜单',
run: () => {
// menu size: 386.6 x 311 (2020-03-29)
// menu size: 176.65 x 194 (2020-06-09)
const player = dq('.bilibili-player-video-wrap') as HTMLElement
if (!player) {
return
}
const rect = player.getBoundingClientRect()
player.dispatchEvent(new MouseEvent('contextmenu', {
bubbles: true,
cancelable: false,
view: unsafeWindow,
button: 2,
buttons: 0,
clientX: rect.x + rect.width / 2 - 176.65 / 2,
clientY: rect.y + rect.height / 2 - 194 / 2,
}))
},
},
seekBegin: {
displayName: '回开头',
run: () => {
if (!unsafeWindow.player) {
return
}
unsafeWindow.player.play()
setTimeout(() => {
unsafeWindow.player.seek(0)
const toastText = dq('.bilibili-player-video-toast-bottom .bilibili-player-video-toast-item:first-child .bilibili-player-video-toast-item-text span:nth-child(2)')
if (toastText) {
toastText.textContent = ' 00:00'
}
})
},
},
}
export const [actions] = registerAndGetData('keymap.actions', builtInActions)