Youtube Player Speed Slider

Add Speed Slider to Youtube Player Settings

当前为 2024-11-14 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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