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