Vimeo Player Speed Slider

Add Speed Slider to Vimeo Player Settings

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Vimeo Player Speed Slider
// @namespace    lukaszmical.pl
// @version      1.1.3
// @description  Add Speed Slider to Vimeo Player Settings
// @author       Łukasz Micał
// @include      https://*.vimeo.com/*
// @include      https://vimeo.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=vimeo.com
// @grant        none
// ==/UserScript==


// apps/vimeo-speed-slider/src/components/Elements.ts
var Elements = 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() {
    return _Elements.menuItem()?.querySelector("span");
  }
  static menuItemWithLabel(labels) {
    const optionItems = [
      ...document.querySelectorAll(
        '[data-menu="prefs"] [class^=MenuOption_module_option]'
      )
    ];
    return optionItems.find(
      (e) => e.id !== MenuItem.ID && labels.some((text) => e.innerText.includes(text))
    );
  }
  static menuSpeedItem() {
    return _Elements.menuItemWithLabel([
      "Speed",
      "Velocidad",
      "Geschwindigkeit",
      "Vitesse",
      "Velocidade",
      "\u30B9\u30D4\u30FC\u30C9",
      "\uC18D\uB3C4"
    ]);
  }
  static menuQualityItem() {
    return _Elements.menuItemWithLabel([
      "Quality",
      "Calidad",
      "Qualit\xE4t",
      "Qualit\xE9",
      "Qualidade",
      "\u753B\u8CEA",
      "\uACE0\uD654\uC9C8"
    ]);
  }
  static menuSpeedLabel() {
    return _Elements.menuSpeedItem()?.querySelector("span");
  }
  static video() {
    return _Elements.ref(".vp-video video");
  }
};

// libs/share/src/ui/Dom.ts
var Dom = 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 (item instanceof Component) {
            return item.getElement();
          }
          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 === void 0 || 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;
    }
  }
};

// libs/share/src/ui/Component.ts
var Component = class {
  constructor(tag, props = {}) {
    this.element = 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);
  }
};

// libs/share/src/ui/GlobalStyle.ts
var GlobalStyle = class {
  static addStyle(key, styles) {
    const style = document.getElementById(key) || function() {
      const style2 = document.createElement("style");
      style2.id = key;
      document.head.appendChild(style2);
      return style2;
    }();
    style.textContent = styles;
  }
};

// apps/vimeo-speed-slider/src/components/Slider.ts
var Slider = class _Slider extends Component {
  static {
    this.MIN_VALUE = 0.5;
  }
  static {
    this.MAX_VALUE = 4;
  }
  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.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));
  }
};

// apps/vimeo-speed-slider/src/components/Checkbox.ts
var Checkbox = class extends Component {
  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
      }
    });
  }
  initEvents(onChange) {
    this.event("change", () => onChange(this.element.checked));
  }
  setValue(checked) {
    this.element.checked = checked;
  }
};

// apps/vimeo-speed-slider/src/components/Label.ts
var Label = class extends Component {
  constructor() {
    super("span");
    this.label = "Speed";
    this.speed = "1.0";
  }
  init() {
    const originalItemLabel = Elements.menuSpeedLabel();
    if (originalItemLabel) {
      this.label = originalItemLabel.innerText;
      this.element.className = originalItemLabel.className;
    }
    const itemLabel = 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}`;
  }
};

// apps/vimeo-speed-slider/src/components/MenuItem.ts
var MenuItem = class _MenuItem extends Component {
  constructor(setSpeed, setRemember) {
    super("div", { attrs: { id: _MenuItem.ID } });
    this.checkbox = new Checkbox(false);
    this.slider = new Slider();
    this.label = new Label();
    this.wrapper = 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);
  }
  static {
    this.ID = "vis-menu-speed-item";
  }
  setSpeed(speed) {
    this.slider.setSpeed(speed);
    this.label.setSpeed(speed);
  }
  setRemember(state) {
    this.checkbox.setValue(state);
  }
  mountItem() {
    const originalSpeedItem = Elements.menuSpeedItem();
    const originalQualityItem = Elements.menuQualityItem();
    if (!originalSpeedItem && !originalQualityItem) {
      this.element.parentNode?.removeChild(this.element);
      return;
    }
    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 = Elements.menuItem()?.className || this.element.className;
    }
  }
};

// apps/vimeo-speed-slider/src/components/Player.ts
var Player = class _Player {
  constructor() {
    this.player = null;
    this.speed = 1;
  }
  static {
    this.READY_FLAG = "vis-listener";
  }
  getPlayer() {
    if (!this.player) {
      this.player = 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;
    }
  }
};

// libs/share/src/store/Store.ts
var Store = class {
  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 = void 0) {
    try {
      const data = localStorage.getItem(this.key);
      if (data) {
        return this.decode(data);
      }
      return defaultValue;
    } catch (e) {
      return defaultValue;
    }
  }
  remove() {
    localStorage.removeItem(this.key);
  }
};

// libs/share/src/ui/Observer.ts
var Observer = class {
  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
    });
  }
};

// apps/vimeo-speed-slider/src/controllers/AppController.ts
var AppController = class {
  constructor() {
    this.player = new Player();
    this.videoObserver = new Observer();
    this.menuObserver = new Observer();
    this.rememberSpeed = new Store("vis-remember-speed");
    this.speed = new Store("vis-speed");
    this.item = new 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.video();
    const menu = 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;
  }
};

// apps/vimeo-speed-slider/src/main.ts
var app = new AppController();
var attempt = 0;
function init() {
  if (attempt <= 4 && !app.init()) {
    attempt++;
    window.setTimeout(init, 2e3);
  }
}
init();