// ==UserScript== // @name WASD // @namespace http://github.com/dnzng // @version 0.0.2 // @description Left-Handed Video Shortcuts // @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.timer = null this.duration = 0.5 this.activeClass = 'wasd-indicator-active' this.initialize() } initialize() { this.injectStyle() this.injectElement() } injectStyle() { const style = document.createElement('style') style.id = 'wasd' style.textContent = ` #wasd-indicator { 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 ${this.duration}s ease; position: fixed; left: 10px; bottom: 10px; z-index: -1; } #wasd-indicator.${this.activeClass} \{ opacity: 1; z-index: 99; } ` document.body.appendChild(style) } injectElement() { const el = document.createElement('div') el.id = 'wasd-indicator' document.body.appendChild(this.el = el) } show(text) { 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.allowKeysList = { w: 'W', s: 'S', a: 'A', d: 'D', x: ['关闭', '显示'] } this.bindEvents() } bindEvents() { window.addEventListener('keydown', this.handleKeydown.bind(this), { capture: true }) } handleKeydown(event) { const { key } = event if (this.isTyping()) return const text = this.allowKeysList[key] if (!text) return if (this.isVisible) { if (key === 'x') { this.isVisible = false this.indicator.show(text[0]) } else { this.indicator.show(text) } } else { if (key === 'x') { this.isVisible = true this.indicator.show(text[1]) } } 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 } } 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) } 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()) }) })();