// ==UserScript== // @name Left-Handed Video Shortcuts // @namespace http://github.com/dnzng // @version 0.0.12 // @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 // @downloadURL 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 (typeof text === 'function') { 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()) }) })();