Refactor keymap

This commit is contained in:
the1812 2020-12-22 22:54:09 +08:00
parent 333bb1b47f
commit 0887384456
11 changed files with 325 additions and 236 deletions

View File

@ -25,6 +25,7 @@
"Swiper", "Swiper",
"Violentmonkey", "Violentmonkey",
"Vuex", "Vuex",
"WASD",
"Watchlater", "Watchlater",
"afterbegin", "afterbegin",
"afterend", "afterend",

File diff suppressed because one or more lines are too long

1
min/key-bindings.min.js vendored Normal file
View File

@ -0,0 +1 @@
(()=>(e,t)=>{const n=["shift","alt","ctrl","meta"];const i=_.once((e=>{const t=document.URL.startsWith("https://www.bilibili.com/watchlater/");const i=document.URL.startsWith("https://www.bilibili.com/medialist/play/");const o={enable:true};document.body.addEventListener("keydown",(s=>{if(!o.enable){return}if(t&&document.URL.endsWith("list")){return}if(document.activeElement&&isTyping()){return}const r=s.key.toLowerCase();const l=dq(".bilibili-player-sphere-control");if(l!==null&&l.style.display!=="none"&&["w","a","s","d"].includes(r)){return}e.forEach((e=>{var o;const r=n.some((t=>{const n=e.keys.includes(t);const i=s[t+"Key"];return n!==i}));if(r){return}const l=e.keys.filter((e=>!n.includes(e.toLowerCase()))).map((e=>e.toLowerCase()));const c=l.includes(s.key.toLowerCase())||l.includes(s.code.toLowerCase());if(!c){return}if((o=e===null||e===void 0?void 0:e.prevent)!==null&&o!==void 0?o:true){s.stopPropagation();s.preventDefault()}e.action({binding:e,isWatchlater:t,isMediaList:i,event:s})}))}));return o}));return{export:{loadKeyBindings:i}}})();

2
min/keymap.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,6 @@
// ==UserScript== // ==UserScript==
// @name Bilibili Evolved (Preview) // @name Bilibili Evolved (Preview)
// @version 1.11.8 // @version 1.11.9
// @description Bilibili Evolved 的预览版, 可以抢先体验新功能. // @description Bilibili Evolved 的预览版, 可以抢先体验新功能.
// @author Grant Howard, Coulomb-G // @author Grant Howard, Coulomb-G
// @copyright 2020, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G) // @copyright 2020, Grant Howard (https://github.com/the1812) & Coulomb-G (https://github.com/Coulomb-G)

View File

@ -250,6 +250,7 @@ export const settings = {
rememberVideoSpeedList: {}, rememberVideoSpeedList: {},
rememberVideoSpeed: false, rememberVideoSpeed: false,
extendVideoSpeed: true, extendVideoSpeed: true,
customKeyBindings: {},
cache: {}, cache: {},
} }
const fixedSettings = { const fixedSettings = {

1
src/global.d.ts vendored
View File

@ -525,6 +525,7 @@ declare global {
}, },
rememberVideoSpeed: boolean, rememberVideoSpeed: boolean,
extendVideoSpeed: boolean, extendVideoSpeed: boolean,
customKeyBindings: { [actionName: string]: string }
} }
const GM_info: MonkeyInfo const GM_info: MonkeyInfo
function GM_xmlhttpRequest(details: MonkeyXhrDetails): { abort: () => void } function GM_xmlhttpRequest(details: MonkeyXhrDetails): { abort: () => void }

View File

@ -1,232 +0,0 @@
const supportedUrls = [
'https://www.bilibili.com/bangumi/',
'https://www.bilibili.com/video/',
'https://www.bilibili.com/watchlater/',
'https://www.bilibili.com/medialist/play/',
]
let enabled = true;
(async () => {
const { VideoSpeedController } = await import("../video/default-video-speed")
if (supportedUrls.some(url => document.URL.startsWith(url))) {
const clickableKeymap = {
f: '.bilibili-player-video-fullscreen', // 全屏
w: '.bilibili-player-video-web-fullscreen', // 网页全屏
t: '.bilibili-player-video-btn-widescreen', // 宽屏
// r: '.bilibili-player-video-btn-repeat', // 切换循环模式
m: '.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume', // 切换静音
p: '.bilibili-player-video-btn-pip', // 画中画
// l: '.video-toolbar .like', // 点赞
c: '.video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)', // 投币
s: '.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)', // 收藏
' ': '.bilibili-player-video-btn-start', // 砸瓦撸多
} as { [key: string]: string }
/** 长按`L`三连使用的记忆变量 */
let likeClick = true
/** 在稍后再看页面里, 记录当前视频是否赞过 */
let liked = false
const isWatchlater = document.URL.startsWith('https://www.bilibili.com/watchlater/')
const isMediaList = document.URL.startsWith('https://www.bilibili.com/medialist/play/')
if (isWatchlater) {
Observer.videoChange(() => {
Ajax.getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/archive/has/like?aid=${unsafeWindow.aid}`).then(json => {
liked = Boolean(json.data)
})
})
}
/** 播放速度提示框用的`setTimeout`句柄 */
let showPlaybackTipOldTimeout: number
/**
*
* @param speed
*/
const showPlaybackTip = (speed: number) => {
let tip = dq('.keymap-playback-tip') as HTMLDivElement
if (!tip) {
const player = dq('.bilibili-player-video-wrap')
if (!player) {
return
}
player.insertAdjacentHTML('afterbegin', /*html*/`
<div class="keymap-playback-tip-container">
<i class="mdi mdi-fast-forward"></i>
<div class="keymap-playback-tip"></div>x
</div>
`)
resources.applyStyleFromText(`
.keymap-playback-tip-container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 8px 16px;
background-color: #000A;
color: white;
pointer-events: none;
opacity: 0;
z-index: 100;
display: flex;
align-items: center;
font-size: 14pt;
border-radius: 4px;
transition: .2s ease-out;
}
.keymap-playback-tip-container.show {
opacity: 1;
}
.keymap-playback-tip-container i {
line-height: 1;
margin-right: 8px;
font-size: 18pt;
}
`, 'keymapStyle')
tip = dq('.keymap-playback-tip') as HTMLDivElement
}
tip.innerHTML = speed.toString()
if (showPlaybackTipOldTimeout) {
clearTimeout(showPlaybackTipOldTimeout)
}
(dq('.keymap-playback-tip-container') as HTMLDivElement).classList.add('show')
showPlaybackTipOldTimeout = window.setTimeout(() => {
(dq('.keymap-playback-tip-container') as HTMLDivElement).classList.remove('show')
}, 2000)
}
document.body.addEventListener('keydown', async e => {
if (!enabled) {
return
}
if (isWatchlater && document.URL.endsWith('list')) {
return
}
if (document.activeElement && isTyping()) {
return
}
const key = e.key.toLowerCase()
const panoramaControl = dq('.bilibili-player-sphere-control') as HTMLElement
if (panoramaControl !== null && panoramaControl.style.display !== 'none' && ['w', 'a', 's', 'd'].includes(key)) {
return
}
const noModifyKeys = !e.shiftKey && !e.altKey && !e.ctrlKey && !e.metaKey
if (key in clickableKeymap && noModifyKeys) {
const element = dq(clickableKeymap[key]) as HTMLElement
if (!element) {
return
}
e.stopPropagation()
e.preventDefault()
element.click()
} else if (key === 'd' && noModifyKeys) { // 切换弹幕开关
const checkbox = dq('.bilibili-player-video-danmaku-switch input') as HTMLInputElement
if (!checkbox) {
return
}
e.stopPropagation()
e.preventDefault()
checkbox.checked = !checkbox.checked
raiseEvent(checkbox, 'change')
} else if (key === 'j' && noModifyKeys) { // 绯红之王
const video = dq('.bilibili-player-video video') as HTMLVideoElement
video.currentTime += settings.keymapJumpSeconds
e.stopPropagation()
e.preventDefault()
} else if (key === 'l' && noModifyKeys) {
if (isWatchlater) {
const formData = {
aid: unsafeWindow.aid,
/** `1`点赞; `2`取消赞 */
like: liked ? 2 : 1,
csrf: getCsrf(),
}
Ajax.postTextWithCredentials(`https://api.bilibili.com/x/web-interface/archive/like`, Object.entries(formData).map(([k, v]) => `${k}=${v}`).join('&')).then(() => {
liked = !liked
if (liked) {
Toast.success(`已点赞`, `快捷键扩展`, 1000)
} else {
Toast.success(`已取消点赞`, `快捷键扩展`, 1000)
}
})
} else if (isMediaList) {
const likeButton = dq('.play-options-ul > li:first-child') as HTMLLIElement
if (likeButton) {
likeButton.click()
}
} else {
const likeButton = dq('.video-toolbar .like') as HTMLSpanElement
e.preventDefault()
const fireEvent = (name: string, args: Event) => {
const event = new CustomEvent(name, args)
likeButton.dispatchEvent(event)
}
likeClick = true
setTimeout(() => likeClick = false, 200)
fireEvent('mousedown', e)
document.body.addEventListener('keyup', e => {
e.preventDefault()
fireEvent('mouseup', e)
if (likeClick) {
fireEvent('click', e)
}
}, { once: true })
}
} else if (key === '`' && noModifyKeys) {
// 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
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
}))
} else if (e.shiftKey) {
const containerElement = await SpinQuery.select(`.${VideoSpeedController.classNameMap.speedContainer}`) as HTMLElement
const videoElement = await SpinQuery.select(`.${VideoSpeedController.classNameMap.video} video`) as HTMLVideoElement
const controller = new VideoSpeedController(containerElement, videoElement, 1)
const playbackRates = VideoSpeedController.supportedRates
let preventDefault = true
if (key === '>' || key === 'ArrowUp'.toLowerCase()) { // 天堂制造
controller.setVideoSpeed(playbackRates.find(it => it > controller.playbackRate) || playbackRates[playbackRates.length - 1])
showPlaybackTip(controller.playbackRate)
} else if (key === '<' || key === 'ArrowDown'.toLowerCase()) { // 时间减速
controller.setVideoSpeed([...playbackRates].reverse().find(it => it < controller.playbackRate) || playbackRates[0])
showPlaybackTip(controller.playbackRate)
} else if (key === '?') { // 重置速度
controller.reset()
showPlaybackTip(controller.playbackRate)
} else if (key === 'w') { // 稍后再看
const watchlater = dq('.video-toolbar .ops .watchlater, .more-ops-list .ops-watch-later, .video-toolbar-module .see-later-box') as HTMLElement
if (watchlater !== null) {
watchlater.click()
}
} else if (key === 'j') { // 败者食尘
videoElement.currentTime -= settings.keymapJumpSeconds
} else if (key === 's') { // 快速收藏
const quickFavorite = dq('.quick-favorite') as HTMLElement
if (quickFavorite) {
quickFavorite.click()
}
} else {
preventDefault = false
}
if (preventDefault) {
e.stopPropagation()
e.preventDefault()
}
}
})
}
})()
export default {
reload: () => enabled = true,
unload: () => enabled = false,
}

View File

@ -0,0 +1,72 @@
export type KeyBindingAction = (context: {
binding: KeyBinding
event: KeyboardEvent
isWatchlater: boolean
isMediaList: boolean
}) => void
export interface KeyBinding {
keys: string[]
action: KeyBindingAction
prevent?: boolean
}
const modifyKeys = [
'shift',
'alt',
'ctrl',
'meta',
]
export const loadKeyBindings = _.once((bindings: KeyBinding[]) => {
const isWatchlater = document.URL.startsWith('https://www.bilibili.com/watchlater/')
const isMediaList = document.URL.startsWith('https://www.bilibili.com/medialist/play/')
const config = {
enable: true
}
document.body.addEventListener('keydown', (e: KeyboardEvent & { [key: string]: boolean }) => {
if (!config.enable) {
return
}
// 稍后再看页面无视快捷键
if (isWatchlater && document.URL.endsWith('list')) {
return
}
// 打字时无视快捷键
if (document.activeElement && isTyping()) {
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
}
bindings.forEach(binding => {
const modifyKeyNotMatch = modifyKeys.some(m => {
const needModifyKey = binding.keys.includes(m)
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
}
if (binding?.prevent ?? true) {
e.stopPropagation()
e.preventDefault()
}
binding.action({
binding,
isWatchlater,
isMediaList,
event: e,
})
})
})
return config
})

245
src/video/keymap/keymap.ts Normal file
View File

@ -0,0 +1,245 @@
import { VideoSpeedController } from '../default-video-speed'
import { KeyBinding, KeyBindingAction } from './key-bindings'
const supportedUrls = [
'https://www.bilibili.com/bangumi/',
'https://www.bilibili.com/video/',
'https://www.bilibili.com/watchlater/',
'https://www.bilibili.com/medialist/play/',
]
let config: { enable: boolean }
if (supportedUrls.some(url => document.URL.startsWith(url))) {
const clickElement = (target: string | HTMLElement) => {
return () => {
if (typeof target === 'string') {
(dq(target) as HTMLElement)?.click()
} else {
target.click()
}
}
}
/** 播放速度提示框用的`setTimeout`句柄 */
let showPlaybackTipOldTimeout: number
/**
*
* @param speed
*/
const showPlaybackTip = (speed: number) => {
let tip = dq('.keymap-playback-tip') as HTMLDivElement
if (!tip) {
const player = dq('.bilibili-player-video-wrap')
if (!player) {
return
}
player.insertAdjacentHTML('afterbegin', /*html*/`
<div class="keymap-playback-tip-container">
<i class="mdi mdi-fast-forward"></i>
<div class="keymap-playback-tip"></div>x
</div>
`)
resources.applyStyleFromText(`
.keymap-playback-tip-container {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
padding: 8px 16px;
background-color: #000A;
color: white;
pointer-events: none;
opacity: 0;
z-index: 100;
display: flex;
align-items: center;
font-size: 14pt;
border-radius: 4px;
transition: .2s ease-out;
}
.keymap-playback-tip-container.show {
opacity: 1;
}
.keymap-playback-tip-container i {
line-height: 1;
margin-right: 8px;
font-size: 18pt;
}
`, 'keymapStyle')
tip = dq('.keymap-playback-tip') as HTMLDivElement
}
tip.innerHTML = speed.toString()
if (showPlaybackTipOldTimeout) {
clearTimeout(showPlaybackTipOldTimeout)
}
(dq('.keymap-playback-tip-container') as HTMLDivElement).classList.add('show')
showPlaybackTipOldTimeout = window.setTimeout(() => {
(dq('.keymap-playback-tip-container') as HTMLDivElement).classList.remove('show')
}, 2000)
}
const videoSpeed = (controllerAction: (controller: VideoSpeedController, rates: number[]) => void) => {
return async () => {
const attr = await import('../default-video-speed')
console.log(attr)
const { VideoSpeedController } = await import('../default-video-speed')
const containerElement = dq(`.${VideoSpeedController.classNameMap.speedContainer}`) as HTMLElement
const videoElement = dq(`.${VideoSpeedController.classNameMap.video} video`) as HTMLVideoElement
if (!containerElement || !videoElement) {
return
}
const controller = new VideoSpeedController(containerElement, videoElement, 1)
controllerAction(controller, VideoSpeedController.supportedRates)
showPlaybackTip(controller.playbackRate)
}
}
const actions = {
fullscreen: clickElement('.bilibili-player-video-btn-fullscreen'),
webFullscreen: clickElement('.bilibili-player-video-web-fullscreen'),
wideScreen: clickElement('.bilibili-player-video-btn-widescreen'),
mute: clickElement('.bilibili-player-video-btn-volume .bilibili-player-iconfont-volume'),
pictureInPicture: clickElement('.bilibili-player-video-btn-pip'),
coin: clickElement('.video-toolbar .coin,.tool-bar .coin-info, .video-toolbar-module .coin-box, .play-options-ul > li:nth-child(2)'),
favorite: clickElement('.video-toolbar .collect, .video-toolbar-module .fav-box, .play-options-ul > li:nth-child(3)'),
pause: clickElement('.bilibili-player-video-btn-start'),
like: (() => {
/** 长按`L`三连使用的记忆变量 */
let likeClick = true
/** 在稍后再看页面里, 记录当前视频是否赞过 */
let liked = false
const listenWatchlaterVideoChange = _.once(() => {
Observer.videoChange(() => {
Ajax.getJsonWithCredentials(`https://api.bilibili.com/x/web-interface/archive/has/like?aid=${unsafeWindow.aid}`).then(json => {
liked = Boolean(json.data)
})
})
})
return (({ isWatchlater, isMediaList, event }) => {
if (isWatchlater) {
listenWatchlaterVideoChange()
const formData = {
aid: unsafeWindow.aid,
/** `1`点赞; `2`取消赞 */
like: liked ? 2 : 1,
csrf: getCsrf(),
}
Ajax.postTextWithCredentials(`https://api.bilibili.com/x/web-interface/archive/like`, Object.entries(formData).map(([k, v]) => `${k}=${v}`).join('&')).then(() => {
liked = !liked
if (liked) {
Toast.success(`已点赞`, `快捷键扩展`, 1000)
} else {
Toast.success(`已取消点赞`, `快捷键扩展`, 1000)
}
})
} else if (isMediaList) {
const likeButton = dq('.play-options-ul > li:first-child') as HTMLLIElement
if (likeButton) {
likeButton.click()
}
} else {
const likeButton = dq('.video-toolbar .like') as HTMLSpanElement
event.preventDefault()
const fireEvent = (name: string, args: Event) => {
const event = new CustomEvent(name, args)
likeButton.dispatchEvent(event)
}
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 })
}
}) as KeyBindingAction
})(),
danmaku: () => {
const checkbox = dq('.bilibili-player-video-danmaku-switch input') as HTMLInputElement
if (!checkbox) {
return
}
checkbox.checked = !checkbox.checked
raiseEvent(checkbox, 'change')
},
longJumpBackward: () => {
const video = dq('.bilibili-player-video video') as HTMLVideoElement
video.currentTime -= settings.keymapJumpSeconds
},
longJumpForward: () => {
const video = dq('.bilibili-player-video video') as HTMLVideoElement
video.currentTime += settings.keymapJumpSeconds
},
playerMenu: () => {
// 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
}))
},
watchlater: clickElement('.video-toolbar .ops .watchlater, .more-ops-list .ops-watch-later, .video-toolbar-module .see-later-box'),
quickFavorite: clickElement('.quick-favorite'),
videoSpeedIncrease: videoSpeed((controller, rates) => {
controller.setVideoSpeed(rates.find(it => it > controller.playbackRate) || rates[rates.length - 1])
}),
videoSpeedDecrease: videoSpeed((controller, rates) => {
controller.setVideoSpeed([...rates].reverse().find(it => it < controller.playbackRate) || rates[0])
}),
videoSpeedReset: videoSpeed((controller, rates) => {
controller.reset()
}),
}
const defaultBindings: { [action in keyof typeof actions]: string } = {
fullscreen: 'f',
webFullscreen: 'w',
wideScreen: 't',
mute: 'm',
pictureInPicture: 'p',
coin: 'c',
favorite: 's',
pause: 'space',
like: 'l',
playerMenu: '`',
longJumpForward: 'j',
longJumpBackward: 'shift j',
watchlater: 'shift w',
quickFavorite: 'shift s',
danmaku: 'd',
videoSpeedIncrease: 'shift > ArrowUp',
videoSpeedDecrease: 'shift < ArrowDown',
videoSpeedReset: 'shift ?',
}
const parseBindings = (bindings: { [action: string]: string }) => {
return Object.entries(bindings).map(([actionName, keyString]) => {
const keys = keyString.split(' ')
return {
keys,
action: (actions as any)[actionName] || (() => {}),
} as KeyBinding
})
}
;(async () => {
const { loadKeyBindings } = await import('./key-bindings')
config = loadKeyBindings(parseBindings(
{ ...defaultBindings, ...settings.customKeyBindings }
))
})()
}
export default {
reload: () => config && (config.enable = true),
unload: () => config && (config.enable = false),
}

View File

@ -1 +1 @@
1.11.8 1.11.9