🎉 欢迎访问GreasyFork.Org 镜像站!本镜像站由公众号【爱吃馍】搭建,用于分享脚本。联系邮箱📮

Greasy fork 爱吃馍镜像

Youtube Player Speed Slider

Add Speed Slider to Youtube Player Settings

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

🚀 安装遇到问题?关注公众号获取帮助

公众号二维码

扫码关注【爱吃馍】

回复【脚本】获取最新教程和防失联地址

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Youtube Player Speed Slider
// @namespace    lukaszmical.pl
// @version      1.0.1
// @description  Add Speed Slider to Youtube Player Settings
// @author       Łukasz Micał
// @match        https://*.youtube.com/*
// @match        https://youtube.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=youtube.com
// @grant        none
// ==/UserScript==

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

// apps/youtube-speed-slider/src/components/Icon.ts
var 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';
var Icon = class extends Component {
  constructor() {
    super('div', {
      classes: 'ytp-menuitem-icon',
      children: {
        tag: 'svg',
        attrs: {
          height: '24',
          width: '24',
          viewBox: '0 0 24 24',
        },
        children: {
          tag: 'path',
          attrs: {
            fill: 'white',
            d: iconPath,
          },
        },
      },
    });
  }
};

// apps/youtube-speed-slider/src/components/Label.ts
var Label = class extends Component {
  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}`;
  }
};

// apps/youtube-speed-slider/src/components/Slider.ts
var Slider = class _Slider extends Component {
  static {
    this.MIN_VALUE = 0.5;
  }
  static {
    this.MAX_VALUE = 4;
  }

  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);
  }
};

// apps/youtube-speed-slider/src/components/Checkbox.ts
var Checkbox = class extends Component {
  constructor(checked) {
    super('input', {
      styles: {
        accentColor: '#f00',
        width: '20px',
        height: '20px',
        margin: '0',
        padding: '0',
      },
      attrs: {
        type: 'checkbox',
        title: 'Remember speed',
        checked,
      },
    });
  }

  getValue() {
    return this.element.checked;
  }
};

// apps/youtube-speed-slider/src/components/SpeedMenuItem.ts
var SpeedMenuItem = class _SpeedMenuItem extends Component {
  constructor() {
    super('div', {
      classes: 'ytp-menuitem',
      attrs: {
        id: _SpeedMenuItem.ID,
      },
    });
    this.wrapper = Dom.element('div', 'ytp-menuitem-content');
  }

  static {
    this.ID = 'yts-speed-menu-item';
  }

  addElement(icon, label, slider, checkbox) {
    this.element.append(icon, label, this.wrapper);
    this.wrapper.append(checkbox, slider);
  }
};

// libs/share/src/utils/delay.ts
async function delay(ms = 1e3) {
  return await new Promise((resolve) => {
    setTimeout(resolve, ms);
  });
}

// apps/youtube-speed-slider/src/components/Menu.ts
var Menu = class {
  constructor() {
    this.getMenu();
  }

  getMenu() {
    return document.querySelector('.ytp-settings-menu .ytp-panel-menu');
  }

  getDefaultMenuItem() {
    const defaultSpeedItem = [
      ...document.querySelectorAll('.ytp-menuitem'),
    ].filter((e) => {
      const path = e
        .querySelector('.ytp-menuitem-icon path')
        ?.getAttribute('d');
      return path?.startsWith('M10,8v8l6-4L10,');
    });
    if (defaultSpeedItem.length) {
      return defaultSpeedItem[0];
    }
    return void 0;
  }

  getLabel() {
    const label = this.getDefaultMenuItem()?.querySelector(
      '.ytp-menuitem-label'
    );
    return label?.innerText;
  }

  async reopenMenu() {
    const menuButton = document.querySelector('.ytp-settings-button');
    const menu = this.getMenu();
    if (menu && this.menuHasCustomItem(menu)) {
      return;
    }
    if (menuButton) {
      menu?.style?.setProperty('opacity', '0');
      menuButton.click();
      await delay(50);
      menuButton.click();
      menu?.style?.setProperty('opacity', '1');
      await delay(50);
    }
  }

  menuHasCustomItem(menu) {
    return Boolean(menu.querySelector(`#${SpeedMenuItem.ID}`));
  }

  addCustomSpeedItem(item) {
    const menu = this.getMenu();
    const defaultItem = this.getDefaultMenuItem();
    if (menu === null) {
      return false;
    }
    if (this.menuHasCustomItem(menu)) {
      defaultItem?.parentNode?.removeChild(defaultItem);
      return true;
    }
    if (defaultItem) {
      defaultItem.replaceWith(item.getElement());
    } else {
      menu.appendChild(item.getElement());
    }
    return true;
  }
};

// apps/youtube-speed-slider/src/components/Player.ts
var Player = class _Player {
  constructor(speed) {
    this.speed = speed;
    this.player = null;
    this.setSpeed(this.speed);
  }

  static {
    this.READY_FLAG = 'yts-listener';
  }

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

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

// 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) {

    }
  }

  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);
  }
};

// apps/youtube-speed-slider/src/controllers/AppController.ts
var AppController = class {
  constructor() {
    this.rememberSpeed = new Store('yts-remember-speed');
    this.speed = new Store('yts-speed');
    const initialSpeed = this.getSpeed();
    this.menu = new Menu();
    this.player = new Player(initialSpeed);
    this.speedMenuItem = new SpeedMenuItem();
    this.icon = new Icon();
    this.label = new Label(initialSpeed);
    this.slider = new Slider(initialSpeed);
    this.checkbox = new Checkbox(this.rememberSpeed.get(false));
    this.observer = new 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.MIN_VALUE,
      Math.min(current + diff, 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);
  }
};

// apps/youtube-speed-slider/src/main.ts
var app = new AppController();

async function init() {
  const ok = await app.initApp();
  if (!ok) {
    window.setTimeout(init, 2e3);
  }
}

document.addEventListener('spfdone', init);
init();