Vimeo Player Speed Slider

Add Speed Slider to Vimeo Player Settings

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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