Youtube Player Speed Slider

Add Speed Slider to Youtube Player Settings

目前為 2024-11-14 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
    })();
})();