Vimeo Player Speed Slider

Add Speed Slider to Vimeo Player Settings

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

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Vimeo Player Speed Slider
// @namespace    vimeo_player_speed_slider
// @version      1.1.2
// @description  Add Speed Slider to Vimeo Player Settings
// @author       Łukasz
// @include      https://*.vimeo.com/*
// @include      https://vimeo.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=vimeo.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: 'var(--color-two)',
                            appearance: 'auto',
                            width: '16px',
                            height: '16px',
                            margin: '0',
                            padding: '0',
                        },
                        attrs: {
                            type: 'checkbox',
                            title: 'Remember speed',
                            checked: checked,
                        },
                    });
                }
                initEvents(onChange) {
                    this.event('change', () => onChange(this.element.checked));
                }
                setValue(checked) {
                    this.element.checked = 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 appendChildren(element, children) {
                    if (typeof children === 'string') {
                        element.innerHTML = children;
                    } else if (children) {
                        element.append(
                            ...Dom.array(children).map((item) => {
                                if (
                                    item instanceof HTMLElement ||
                                    item instanceof SVGElement
                                ) {
                                    return item;
                                }
                                if (Dom.isSvgItem(item)) {
                                    return Dom.createSvg(item);
                                }
                                return Dom.create(item);
                            }),
                        );
                    }
                }
                static create(data) {
                    const element = document.createElement(data.tag);
                    Dom.appendChildren(element, data.children);
                    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,
                    );
                    Dom.appendChildren(element, data.children);
                    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);
                    }
                }
                static isSvgItem(item) {
                    try {
                        const element = document.createElementNS(
                            'http://www.w3.org/2000/svg',
                            item.tag,
                        );
                        return element.namespaceURI === 'http://www.w3.org/2000/svg';
                    } catch (error) {
                        return false;
                    }
                }
            }
            exports.Dom = Dom;
        },

        'Elements.ts': (_unused_module, exports, _require) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.Elements = void 0;
            const MenuItem_1 = _require('MenuItem.ts');
            class Elements {
                static ref(selector) {
                    return document.querySelector(selector);
                }
                static menu() {
                    return Elements.ref(
                        '[data-menu="prefs"] [class^=Menu_module_menuPanel]',
                    );
                }
                static menuItem() {
                    return Elements.ref(
                        '[data-menu="prefs"] [class^=Menu_module_menuPanel] [class^=MenuOption_module_option]',
                    );
                }
                static menuItemLabel() {
                    var _a;
                    return (_a = Elements.menuItem()) === null || _a === void 0
                        ? void 0
                        : _a.querySelector('span');
                }
                static menuItemWithLabel(labels) {
                    const optionItems = [
                        ...document.querySelectorAll(
                            '[data-menu="prefs"] [class^=MenuOption_module_option]',
                        ),
                    ];
                    return optionItems.find(
                        (e) =>
                            e.id !== MenuItem_1.MenuItem.ID &&
                            labels.some((text) => e.innerText.includes(text)),
                    );
                }
                static menuSpeedItem() {
                    return Elements.menuItemWithLabel([
                        'Speed',
                        'Velocidad',
                        'Geschwindigkeit',
                        'Vitesse',
                        'Velocidade',
                        'スピード',
                        '속도',
                    ]);
                }
                static menuQualityItem() {
                    return Elements.menuItemWithLabel([
                        'Quality',
                        'Calidad',
                        'Qualität',
                        'Qualité',
                        'Qualidade',
                        '画質',
                        '고화질',
                    ]);
                }
                static menuSpeedLabel() {
                    var _a;
                    return (_a = Elements.menuSpeedItem()) === null || _a === void 0
                        ? void 0
                        : _a.querySelector('span');
                }
                static video() {
                    return Elements.ref('.vp-video video');
                }
            }
            exports.Elements = Elements;
        },

        'Label.ts': (_unused_module, exports, _require) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.Label = void 0;
            const Component_1 = _require('Component.ts');
            const Elements_1 = _require('Elements.ts');
            class Label extends Component_1.default {
                constructor() {
                    super('span');
                    this.label = 'Speed';
                    this.speed = '1.0';
                }
                init() {
                    const originalItemLabel = Elements_1.Elements.menuSpeedLabel();
                    if (originalItemLabel) {
                        this.label = originalItemLabel.innerText;
                        this.element.className = originalItemLabel.className;
                    }
                    const itemLabel = Elements_1.Elements.menuItemLabel();
                    if (itemLabel) {
                        this.element.className = itemLabel.className;
                    }
                    this.render();
                }
                setSpeed(speed) {
                    this.speed = speed.toFixed(1);
                    this.render();
                }
                render() {
                    this.element.innerText = `${this.label}: ${this.speed}`;
                }
            }
            exports.Label = Label;
        },

        'MenuItem.ts': (_unused_module, exports, _require) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.MenuItem = void 0;
            const Component_1 = _require('Component.ts');
            const Elements_1 = _require('Elements.ts');
            const Slider_1 = _require('Slider.ts');
            const Checkbox_1 = _require('Checkbox.ts');
            const Label_1 = _require('Label.ts');
            const Dom_1 = _require('Dom.ts');
            class MenuItem extends Component_1.default {
                constructor(setSpeed, setRemember) {
                    super('div', {attrs: {id: MenuItem.ID}});
                    this.checkbox = new Checkbox_1.Checkbox(false);
                    this.slider = new Slider_1.Slider();
                    this.label = new Label_1.Label();
                    this.wrapper = Dom_1.Dom.create({
                        tag: 'div',
                        styles: {
                            display: 'flex',
                            alignItems: 'center',
                        },
                    });
                    this.wrapper.append(
                        this.checkbox.getElement(),
                        this.slider.getElement(),
                    );
                    this.element.append(this.label.getElement(), this.wrapper);
                    this.slider.initEvents(setSpeed);
                    this.checkbox.initEvents(setRemember);
                }
                setSpeed(speed) {
                    this.slider.setSpeed(speed);
                    this.label.setSpeed(speed);
                }
                setRemember(state) {
                    this.checkbox.setValue(state);
                }
                mountItem() {
                    var _a, _b;
                    const originalSpeedItem = Elements_1.Elements.menuSpeedItem();
                    const originalQualityItem =
                        Elements_1.Elements.menuQualityItem();
                    if (!originalSpeedItem && !originalQualityItem) {
                        (_a = this.element.parentNode) === null || _a === void 0
                            ? void 0
                            : _a.removeChild(this.element);
                        return;
                    }
                    originalSpeedItem === null || originalSpeedItem === void 0
                        ? void 0
                        : originalSpeedItem.style.setProperty('display', 'none');
                    if (!this.element.parentNode) {
                        if (originalSpeedItem) {
                            originalSpeedItem.after(this.element);
                        } else if (originalQualityItem) {
                            originalQualityItem.after(this.element);
                        }
                        this.label.init();
                        this.element.className =
                            ((_b = Elements_1.Elements.menuItem()) === null ||
                            _b === void 0
                                ? void 0
                                : _b.className) || this.element.className;
                    }
                }
            }
            exports.MenuItem = MenuItem;
            MenuItem.ID = 'vis-menu-speed-item';
        },

        'Player.ts': (_unused_module, exports, _require) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.Player = void 0;
            const Elements_1 = _require('Elements.ts');
            class Player {
                constructor() {
                    this.player = null;
                    this.speed = 1;
                }
                getPlayer() {
                    if (!this.player) {
                        this.player = Elements_1.Elements.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 = 'vis-listener';
        },

        'Slider.ts': (_unused_module, exports, _require) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.Slider = void 0;
            const Component_1 = _require('Component.ts');
            const GlobalStyle_1 = _require('GlobalStyle.ts');
            class Slider extends Component_1.default {
                constructor() {
                    super('input', {
                        classes: 'vis-slider',
                        attrs: {
                            type: 'range',
                            min: Slider.MIN_VALUE,
                            max: Slider.MAX_VALUE,
                            step: 0.05,
                        },
                        styles: {
                            background: '#ffffff66',
                            width: 'calc(100% - 30px)',
                            height: '6px',
                            outline: 'none',
                            margin: '0 10px',
                            padding: '0',
                            borderRadius: '3px',
                            minWidth: '150px',
                        },
                    });
                    GlobalStyle_1.GlobalStyle.addStyle(
                        'vis-slider',
                        `input[type='range'].vis-slider {
              -webkit-appearance: none;
            }

            input[type='range'].vis-slider::-moz-range-thumb ,
            input[type='range'].vis-slider::-webkit-slider-thumb {
              -webkit-appearance: none;
              appearance: none;
              width: 12px;
              height: 12px;
              border-radius: 6px;
              background: #fff;
              cursor: pointer;
              margin-top: -2px;
            }`,
                    );
                }
                initEvents(onChange) {
                    this.event('change', () => onChange(this.getSpeed()));
                    this.event('input', () => onChange(this.getSpeed()));
                    this.event('wheel', (event) => {
                        event.stopPropagation();
                        event.preventDefault();
                        const diff = event.deltaY > 0 ? -0.05 : 0.05;
                        onChange(this.getSpeed() + diff);
                        return false;
                    });
                }
                setSpeed(speed) {
                    this.updateBg(speed);
                    this.element.value = speed.toString();
                }
                getSpeed() {
                    return parseFloat(this.element.value);
                }
                updateBg(value) {
                    const progress =
                        ((value - Slider.MIN_VALUE) /
                            (Slider.MAX_VALUE - Slider.MIN_VALUE)) *
                        100;
                    this.element.style.background =
                        'linear-gradient(to right, COLOR1 0%, COLOR1 STEP%, COLOR2 STEP%, COLOR2 100%)'
                            .replaceAll('COLOR1', 'var(--color-two)')
                            .replaceAll('COLOR2', '#ffffff66')
                            .replaceAll('STEP', progress.toFixed(1));
                }
            }
            exports.Slider = Slider;
            Slider.MIN_VALUE = 0.5;
            Slider.MAX_VALUE = 4;
        },

        'AppController.ts': (_unused_module, exports, _require) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.AppController = void 0;
            const Store_1 = _require('Store.ts');
            const MenuItem_1 = _require('MenuItem.ts');
            const Player_1 = _require('Player.ts');
            const Observer_1 = _require('Observer.ts');
            const Elements_1 = _require('Elements.ts');
            class AppController {
                constructor() {
                    this.player = new Player_1.Player();
                    this.videoObserver = new Observer_1.Observer();
                    this.menuObserver = new Observer_1.Observer();
                    this.rememberSpeed = new Store_1.Store('vis-remember-speed');
                    this.speed = new Store_1.Store('vis-speed');
                    this.item = new MenuItem_1.MenuItem(
                        this.setSpeed.bind(this),
                        this.setRemember.bind(this),
                    );
                    this.setSpeed(this.getSpeed());
                    this.setRemember(this.rememberSpeed.get(false));
                }
                setSpeed(speed) {
                    this.speed.set(speed);
                    this.player.setSpeed(speed);
                    this.item.setSpeed(speed);
                }
                setRemember(state) {
                    this.rememberSpeed.set(state);
                    this.item.setRemember(state);
                }
                getSpeed() {
                    return this.rememberSpeed.get(false) ? this.speed.get(1) : 1;
                }
                mount() {
                    this.item.mountItem();
                }
                init() {
                    const video = Elements_1.Elements.video();
                    const menu = Elements_1.Elements.menu();
                    if (video && menu) {
                        this.videoObserver.start(video, this.mount.bind(this));
                        this.menuObserver.start(menu, this.mount.bind(this));
                        this.mount();
                        this.setSpeed(this.getSpeed());
                        return true;
                    }
                    return false;
                }
            }
            exports.AppController = AppController;
        },

        'GlobalStyle.ts': (_unused_module, exports) => {
            Object.defineProperty(exports, '__esModule', {value: true});
            exports.GlobalStyle = void 0;
            class GlobalStyle {
                static addStyle(key, styles) {
                    const style =
                        document.getElementById(key) ||
                        (function () {
                            const style = document.createElement('style');
                            style.id = key;
                            document.head.appendChild(style);
                            return style;
                        })();
                    style.textContent = styles;
                }
            }
            exports.GlobalStyle = GlobalStyle;
        },

        '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;
        },
    };

    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();
        let attempt = 0;
        function init() {
            if (attempt <= 4 && !app.init()) {
                attempt++;
                window.setTimeout(init, 2000);
            }
        }
        init();
    })();
})();