// ==UserScript== // @name Left-Handed Video Shortcuts // @namespace http://github.com/dnzng // @version 0.0.9 // @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} \{ display: flex; justify-content: center; align-items: center; width: 50px; height: 50px; 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: '⬆', s: '⬇︎', a: '⬅︎', d: '➡︎', 1: 'x1', 2: 'x2', 3: 'x3', 4: 'x4', 5: 'x5', r: () => `x${this.rate}`, x: ['Hide', 'Show'] } this.bindEvents() } bindEvents() { window.addEventListener('keydown', this.handleKeydown.bind(this), { capture: true }) } handleKeydown(event) { const { key } = event let text = this.allowKeysList[key] if (this.isTyping()) return if (!text) 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() break } if (this.isVisible) { if (key === 'x') { this.isVisible = false text = text[0] } if (key === 'r') { 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) } setPlaybackRate(rate) { this.rate = this.media.playbackRate = rate } increasePlaybackRate() { this.setPlaybackRate(this.rate + this.rateStep) } 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()) }) })();