Youtube Player Speed Slider

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();
    })();
})();