Greasy Fork is available in English.
专为左手人士设计的视频快捷键
当前为
// ==UserScript==
// @name Left-Handed Video Shortcuts
// @namespace http://github.com/dnzng
// @version 0.0.11
// @description A set of shortcuts designed for the Left-Hand style while watching videos
// @description:zh-CN 专为左手人士设计的视频快捷键
// @author Dylan Zhang
// @license MIT
// @include https://*.youtube.com/*
// @include https://*.bilibili.com/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant none
// ==/UserScript==
(function() {
'use strict';
/* utilities */
const utils = {
ensureCondition(condition, maxAttempts = 600 /* 10s */, failureMessage) {
return new Promise((resolve, reject) => {
let attempts = 0
const detect = () => {
const result = condition()
if (result) {
resolve(result)
} else if (attempts < maxAttempts) {
attempts++
requestAnimationFrame(detect)
} else {
reject(new Error(failureMessage))
}
}
requestAnimationFrame(detect)
})
},
ensureElement(selector, maxAttempts = 600) {
return utils.ensureCondition(
() => document.querySelector(selector),
maxAttempts,
`Could not detect ${selector} after ${maxAttempts} attempts`
)
}
}
class Indicator {
constructor() {
this.el = null
this.hasInited = false
this.timer = null
this.duration = 0.5
this.wrapperClass = 'wasd-indicator'
this.activeClass = 'wasd-indicator-active'
}
initialize() {
this.injectStyle()
this.injectElement()
}
injectStyle() {
const { wrapperClass, activeClass, duration } = this
const style = document.createElement('style')
style.id = wrapperClass
style.textContent = `
#${wrapperClass} \{
box-sizing: border-box;
display: flex;
justify-content: center;
align-items: center;
min-width: 50px;
height: 50px;
padding: 0 10px;
background: #000;
font-size: 18px;
font-weight: bold;
color: #fff;
border-radius: 10px;
opacity: 0;
transition: opacity ${duration}s ease;
position: fixed;
left: 10px;
bottom: 10px;
z-index: -1;
}
#${wrapperClass}\.${activeClass} \{
opacity: 1;
z-index: 99;
}
`
document.body.appendChild(style)
}
injectElement() {
const el = document.createElement('div')
el.id = this.wrapperClass
document.body.appendChild(this.el = el)
}
show(text) {
if (!this.hasInited) {
this.hasInited = true
this.initialize()
// Force to excute a reflow to
// ensure that the animation can be run at the first time
void this.el.offsetWidth
}
const { el, activeClass } = this
el.textContent = text
el.classList.add(activeClass)
if (this.timer) clearTimeout(this.timer)
this.timer = setTimeout(() => {
el.classList.remove(activeClass)
this.timer = null
}, 800)
}
}
class Shortcuts {
constructor(meida, indicator) {
this.media = meida
this.indicator = indicator
this.isVisible = false
this.seekStep = 5
this.volumeStep = 0.1
this.rate = 1
this.rateStep = 0.25
this.allowKeysList = {
w: () => {
const volume = this.getVolume()
let text = '⬆'
if (volume === 1) text += 'Max'
return text
},
s: () => {
const volume = this.getVolume()
let text = '⬇︎'
if (volume === 0) text += 'Min'
return text
},
a: '⬅︎',
d: '➡︎',
1: 'x1',
2: 'x2',
3: 'x3',
4: 'x4',
5: 'x5',
r: () => `x${this.rate}`,
x: ['Off', 'On']
}
this.bindEvents()
}
bindEvents() {
window.addEventListener('keydown', this.handleKeydown.bind(this), { capture: true })
}
handleKeydown(event) {
if (this.isTyping()) return
// the key is uppercase while pressing with the shift key
const key = event.key.toLowerCase()
let text = this.allowKeysList[key]
// not in the allowed keys or with ctrl/command key
if (!text || event.metaKey || event.ctrlKey) return
event.stopImmediatePropagation()
switch(key) {
case 'w': // increase volume
this.increaseVolume()
break
case 's': // decrease volume
this.decreaseVolume()
break
case 'a': // rewind
this.seek(this.getCurrentTime() - this.seekStep)
break
case 'd': // fast forward
this.seek(this.getCurrentTime() + this.seekStep)
break
case '1':
case '2':
case '3':
case '4':
case '5':
this.setPlaybackRate(parseInt(event.key))
break
case 'r':
this.increasePlaybackRate(event.shiftKey)
break
}
if (this.isVisible) {
if (key === 'x') {
this.isVisible = false
text = text[0]
}
if (['w', 's', 'r'].includes(key)) {
text = text()
}
this.indicator.show(text)
} else {
if (key === 'x') {
this.isVisible = true
this.indicator.show(text[1])
}
}
}
seek(time) {
this.media.currentTime = time
}
getCurrentTime() {
return this.media.currentTime
}
increaseVolume() {
this.media.volume = Math.min(this.media.volume + this.volumeStep, 1)
}
decreaseVolume() {
this.media.volume = Math.max(this.media.volume - this.volumeStep, 0)
}
getVolume() {
return this.media.volume
}
setPlaybackRate(rate) {
this.rate = this.media.playbackRate = rate
}
increasePlaybackRate(withShift) {
console.log(withShift)
const { rate, rateStep } = this
const finalRate = withShift
? Math.max(rate - rateStep, 1)
: rate + rateStep
this.setPlaybackRate(finalRate)
}
togglePlay() {
const { media } = this
media.paused
? media.play()
: media.pause()
}
isTyping() {
const activeElement = document.activeElement
return activeElement instanceof HTMLInputElement ||
activeElement instanceof HTMLTextAreaElement ||
activeElement.isContentEditable === true
}
}
utils.ensureElement('video').then(video => {
new Shortcuts(video, new Indicator())
})
})();