您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add Speed Slider to Youtube Player Settings
当前为
// ==UserScript== // @name Youtube Player Speed Slider // @namespace youtube_player_speed_slider // @version 1.0.0 // @description Add Speed Slider to Youtube Player Settings // @author Łukasz // @match https://*.youtube.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=youtube.com // @grant none // ==/UserScript== (() => { 'use strict'; var _modules = { 'Checkbox.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Checkbox = void 0; const Component_1 = _require('Component.ts'); class Checkbox extends Component_1.default { constructor(checked) { super('input', { styles: { accentColor: '#f00', width: '20px', height: '20px', margin: '0', padding: '0', }, attrs: { type: 'checkbox', title: 'Remember speed', checked: checked, }, }); } getValue() { return this.element.checked; } } exports.Checkbox = Checkbox; }, 'Component.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); const Dom_1 = _require('Dom.ts'); class Component { constructor(tag, props = {}) { this.element = Dom_1.Dom.create({tag, ...props}); } addClassName(...className) { this.element.classList.add(...className); } event(event, callback) { this.element.addEventListener(event, callback); } getElement() { return this.element; } mount(parent) { parent.appendChild(this.element); } } exports['default'] = Component; }, 'Dom.ts': (_unused_module, exports) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Dom = void 0; class Dom { static create(data) { const element = document.createElement(data.tag); if (typeof data.children === 'string') { element.innerHTML = data.children; } else if (data.children) { element.append( ...Dom.array(data.children).map((item) => item instanceof HTMLElement || item instanceof SVGElement ? item : Dom.create(item), ), ); } Dom.applyClass(element, data.classes); Dom.applyAttrs(element, data.attrs); Dom.applyEvents(element, data.events); Dom.applyStyles(element, data.styles); return element; } static element(tag, classes, children) { return Dom.create({tag, classes, children}); } static createSvg(data) { const element = document.createElementNS( 'http://www.w3.org/2000/svg', data.tag, ); if (typeof data.children === 'string') { element.innerHTML = data.children; } else if (data.children) { element.append( ...Dom.array(data.children).map((item) => item instanceof SVGElement ? item : Dom.createSvg(item), ), ); } Dom.applyClass(element, data.classes); Dom.applyAttrs(element, data.attrs); Dom.applyEvents(element, data.events); Dom.applyStyles(element, data.styles); return element; } static array(element) { return Array.isArray(element) ? element : [element]; } static elementSvg(tag, classes, children) { return Dom.createSvg({tag, classes, children}); } static applyAttrs(element, attrs) { if (attrs) { Object.entries(attrs).forEach(([key, value]) => { if (value === undefined || value === false) { element.removeAttribute(key); } else { element.setAttribute(key, `${value}`); } }); } } static applyStyles(element, styles) { if (styles) { Object.entries(styles).forEach(([key, value]) => { const name = key.replace( /[A-Z]/g, (c) => `-${c.toLowerCase()}`, ); element.style.setProperty(name, value); }); } } static applyEvents(element, events) { if (events) { Object.entries(events).forEach(([name, callback]) => { element.addEventListener(name, callback); }); } } static applyClass(element, classes) { if (classes) { element.setAttribute('class', classes); } } } exports.Dom = Dom; }, 'Icon.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Icon = void 0; const Component_1 = _require('Component.ts'); const Dom_1 = _require('Dom.ts'); const iconPath = 'M10.01,8v8l6-4L10,8L10,8z M6.3,5L5.7,4.2C7.2,3,9,2.2,11,2l0.1,1C9.3,3.2,7.7,3.9,6.3,5z M5,6.3L4.2,5.7C3,7.2,2.2,9,2,11 l1,.1C3.2,9.3,3.9,7.7,5,6.3z M5,17.7c-1.1-1.4-1.8-3.1-2-4.8L2,13c0.2,2,1,3.8,2.2,5.4L5,17.7z M11.1,21c-1.8-0.2-3.4-0.9-4.8-2 l-0.6,.8C7.2,21,9,21.8,11,22L11.1,21z M22,12c0-5.2-3.9-9.4-9-10l-0.1,1c4.6,.5,8.1,4.3,8.1,9s-3.5,8.5-8.1,9l0.1,1 C18.2,21.5,22,17.2,22,12z'; class Icon extends Component_1.default { constructor() { super('div', { classes: 'ytp-menuitem-icon', children: Dom_1.Dom.createSvg({ tag: 'svg', attrs: { height: '24', width: '24', viewBox: '0 0 24 24', }, children: Dom_1.Dom.createSvg({ tag: 'path', attrs: { fill: 'white', d: iconPath, }, }), }), }); } } exports.Icon = Icon; }, 'Label.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Label = void 0; const Component_1 = _require('Component.ts'); class Label extends Component_1.default { constructor(speed, label = 'Speed') { super('div', {classes: 'ytp-menuitem-label'}); this.speed = '1.0'; this.label = label; this.updateSpeed(speed); } updateLabel(label = 'Speed') { this.label = label; this.updateText(); } updateSpeed(speed) { this.speed = speed.toFixed(1); this.updateText(); } updateText() { this.element.innerText = `${this.label}: ${this.speed}`; } } exports.Label = Label; }, 'Menu.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Menu = void 0; const SpeedMenuItem_1 = _require('SpeedMenuItem.ts'); const delay_1 = _require('delay.ts'); class Menu { constructor() { this.getMenu(); } getMenu() { return document.querySelector( '.ytp-settings-menu .ytp-panel-menu', ); } getDefaultMenuItem() { const defaultSpeedItem = [ ...document.querySelectorAll('.ytp-menuitem'), ].filter((e) => { var _a; const path = (_a = e.querySelector('.ytp-menuitem-icon path')) === null || _a === void 0 ? void 0 : _a.getAttribute('d'); return path === null || path === void 0 ? void 0 : path.startsWith('M10,8v8l6-4L10,'); }); if (defaultSpeedItem.length) { return defaultSpeedItem[0]; } return undefined; } getLabel() { var _a; const label = (_a = this.getDefaultMenuItem()) === null || _a === void 0 ? void 0 : _a.querySelector('.ytp-menuitem-label'); return label === null || label === void 0 ? void 0 : label.innerText; } async reopenMenu() { var _a, _b; const menuButton = document.querySelector( '.ytp-settings-button', ); const menu = this.getMenu(); if (menu && this.menuHasCustomItem(menu)) { return; } if (menuButton) { (_a = menu === null || menu === void 0 ? void 0 : menu.style) === null || _a === void 0 ? void 0 : _a.setProperty('opacity', '0'); menuButton.click(); await (0, delay_1.delay)(50); menuButton.click(); (_b = menu === null || menu === void 0 ? void 0 : menu.style) === null || _b === void 0 ? void 0 : _b.setProperty('opacity', '1'); await (0, delay_1.delay)(50); } } menuHasCustomItem(menu) { return Boolean( menu.querySelector(`#${SpeedMenuItem_1.SpeedMenuItem.ID}`), ); } addCustomSpeedItem(item) { var _a; const menu = this.getMenu(); const defaultItem = this.getDefaultMenuItem(); if (menu === null) { return false; } if (this.menuHasCustomItem(menu)) { (_a = defaultItem === null || defaultItem === void 0 ? void 0 : defaultItem.parentNode) === null || _a === void 0 ? void 0 : _a.removeChild(defaultItem); return true; } if (defaultItem) { defaultItem.replaceWith(item.getElement()); } else { menu.appendChild(item.getElement()); } return true; } } exports.Menu = Menu; }, 'Player.ts': (_unused_module, exports) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Player = void 0; class Player { constructor(speed) { this.speed = speed; this.player = null; this.setSpeed(this.speed); } getPlayer() { if (!this.player) { this.player = document.querySelector('.html5-main-video'); if (this.player) { this.initEvent(this.player); } } return this.player; } initEvent(player) { if (!player.getAttribute(Player.READY_FLAG)) { player.addEventListener( 'ratechange', this.checkPlayerSpeed.bind(this), ); player.setAttribute(Player.READY_FLAG, 'ready'); } } checkPlayerSpeed() { const player = this.getPlayer(); if ( player && Math.abs(player.playbackRate - this.speed) > 0.01 ) { player.playbackRate = this.speed; setTimeout(this.checkPlayerSpeed.bind(this), 200); } } setSpeed(speed) { this.speed = speed; const player = this.getPlayer(); if (player !== null) { player.playbackRate = speed; } } } exports.Player = Player; Player.READY_FLAG = 'yts-listener'; }, 'Slider.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Slider = void 0; const Component_1 = _require('Component.ts'); class Slider extends Component_1.default { constructor(speed) { super('input', { attrs: { type: 'range', min: Slider.MIN_VALUE, max: Slider.MAX_VALUE, step: 0.05, value: speed.toString(), }, styles: { accentColor: '#f00', width: 'calc(100% - 30px)', margin: '0 5px', padding: '0', }, }); } setSpeed(speed) { this.element.value = speed.toString(); } getSpeed() { return parseFloat(this.element.value); } } exports.Slider = Slider; Slider.MIN_VALUE = 0.5; Slider.MAX_VALUE = 4; }, 'SpeedMenuItem.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.SpeedMenuItem = void 0; const Component_1 = _require('Component.ts'); const Dom_1 = _require('Dom.ts'); class SpeedMenuItem extends Component_1.default { constructor() { super('div', { classes: 'ytp-menuitem', attrs: { id: SpeedMenuItem.ID, }, }); this.wrapper = Dom_1.Dom.element('div', 'ytp-menuitem-content'); } addElement(icon, label, slider, checkbox) { this.element.append(icon, label, this.wrapper); this.wrapper.append(checkbox, slider); } } exports.SpeedMenuItem = SpeedMenuItem; SpeedMenuItem.ID = 'yts-speed-menu-item'; }, 'AppController.ts': (_unused_module, exports, _require) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.AppController = void 0; const Icon_1 = _require('Icon.ts'); const Label_1 = _require('Label.ts'); const Slider_1 = _require('Slider.ts'); const Checkbox_1 = _require('Checkbox.ts'); const Store_1 = _require('Store.ts'); const SpeedMenuItem_1 = _require('SpeedMenuItem.ts'); const Menu_1 = _require('Menu.ts'); const Player_1 = _require('Player.ts'); const Observer_1 = _require('Observer.ts'); class AppController { constructor() { this.rememberSpeed = new Store_1.Store('yts-remember-speed'); this.speed = new Store_1.Store('yts-speed'); const initialSpeed = this.getSpeed(); this.menu = new Menu_1.Menu(); this.player = new Player_1.Player(initialSpeed); this.speedMenuItem = new SpeedMenuItem_1.SpeedMenuItem(); this.icon = new Icon_1.Icon(); this.label = new Label_1.Label(initialSpeed); this.slider = new Slider_1.Slider(initialSpeed); this.checkbox = new Checkbox_1.Checkbox( this.rememberSpeed.get(false), ); this.observer = new Observer_1.Observer(); this.speedMenuItem.addElement( this.icon.getElement(), this.label.getElement(), this.slider.getElement(), this.checkbox.getElement(), ); this.initEvents(); } initEvents() { this.slider.event('change', this.sliderChangeEvent.bind(this)); this.slider.event('input', this.sliderChangeEvent.bind(this)); this.slider.event('wheel', this.sliderWheelEvent.bind(this)); this.checkbox.event('change', this.checkboxEvent.bind(this)); document.addEventListener('spfdone', this.initApp.bind(this)); } sliderChangeEvent(_) { this.updateSpeed(this.slider.getSpeed()); } checkboxEvent(_) { this.rememberSpeed.set(this.checkbox.getValue()); } sliderWheelEvent(event) { const current = this.slider.getSpeed(); const diff = event.deltaY > 0 ? -0.05 : 0.05; const value = Math.max( Slider_1.Slider.MIN_VALUE, Math.min(current + diff, Slider_1.Slider.MAX_VALUE), ); if (current != value) { this.slider.setSpeed(value); this.updateSpeed(value); } event.preventDefault(); } updateSpeed(speed) { this.speed.set(speed); this.player.setSpeed(speed); this.label.updateSpeed(speed); } getSpeed() { return this.rememberSpeed.get() ? this.speed.get(1) : 1; } mutationCallback() { this.initApp(); } async initApp() { this.player.setSpeed(this.getSpeed()); await this.menu.reopenMenu(); const label = this.menu.getLabel(); if (label) { this.label.updateLabel(label); } const player = this.player.getPlayer(); if (player) { this.observer.start( player, this.mutationCallback.bind(this), ); } return this.menu.addCustomSpeedItem(this.speedMenuItem); } } exports.AppController = AppController; }, 'Observer.ts': (_unused_module, exports) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Observer = void 0; class Observer { stop() { if (this.observer) { this.observer.disconnect(); } } start(element, callback) { this.stop(); this.observer = new MutationObserver(callback); this.observer.observe(element, { childList: true, subtree: true, attributes: true, characterData: true, attributeOldValue: true, characterDataOldValue: true, }); } } exports.Observer = Observer; }, 'Store.ts': (_unused_module, exports) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.Store = void 0; class Store { constructor(key) { this.key = key; } encode(val) { return JSON.stringify(val); } decode(val) { return JSON.parse(val); } set(value) { try { localStorage.setItem(this.key, this.encode(value)); } catch (e) { return; } } get(defaultValue = undefined) { try { const data = localStorage.getItem(this.key); if (data) { return this.decode(data); } return defaultValue; } catch (e) { return defaultValue; } } remove() { localStorage.removeItem(this.key); } } exports.Store = Store; }, 'delay.ts': (_unused_module, exports) => { Object.defineProperty(exports, '__esModule', {value: true}); exports.delay = void 0; async function delay(ms = 1000) { return await new Promise((resolve) => { setTimeout(resolve, ms); }); } exports.delay = delay; }, }; var _module_cache = {}; function _require(moduleId) { var cachedModule = _module_cache[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = (_module_cache[moduleId] = { exports: {}, }); _modules[moduleId](module, module.exports, _require); return module.exports; } var _exports = {}; (() => { var exports = _exports; var _unused_export; _unused_export = {value: true}; const AppController_1 = _require('AppController.ts'); const app = new AppController_1.AppController(); async function init() { const ok = await app.initApp(); if (!ok) { window.setTimeout(init, 2000); } } document.addEventListener('spfdone', init); init(); })(); })();