// ==UserScript==
// @namespace https://github.com/Sv443-Network/UserUtils
// @exclude *
// @author Sv443
// @supportURL https://github.com/Sv443-Network/UserUtils/issues
// @homepageURL https://github.com/Sv443-Network/UserUtils#readme
// @supportURL https://github.com/Sv443-Network/UserUtils/issues
// ==UserLibrary==
// @name UserUtils
// @description Library with various utilities for userscripts - register listeners for when CSS selectors exist, intercept events, manage persistent user configurations, modify the DOM more easily and more
// @version 2.0.0
// @license MIT
// @copyright Sv443 (https://github.com/Sv443)
// ==/UserScript==
// ==/UserLibrary==
// ==OpenUserJS==
// @author Sv443
// ==/OpenUserJS==
var UserUtils = (function (exports) {
var __defProp = Object.defineProperty;
var __defProps = Object.defineProperties;
var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
var __getOwnPropSymbols = Object.getOwnPropertySymbols;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __propIsEnum = Object.prototype.propertyIsEnumerable;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __spreadValues = (a, b) => {
for (var prop in b || (b = {}))
if (__hasOwnProp.call(b, prop))
__defNormalProp(a, prop, b[prop]);
if (__getOwnPropSymbols)
for (var prop of __getOwnPropSymbols(b)) {
if (__propIsEnum.call(b, prop))
__defNormalProp(a, prop, b[prop]);
}
return a;
};
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
var __publicField = (obj, key, value) => {
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
return value;
};
var __async = (__this, __arguments, generator) => {
return new Promise((resolve, reject) => {
var fulfilled = (value) => {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
};
var rejected = (value) => {
try {
step(generator.throw(value));
} catch (e) {
reject(e);
}
};
var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
step((generator = generator.apply(__this, __arguments)).next());
});
};
// lib/math.ts
function clamp(value, min, max) {
return Math.max(Math.min(value, max), min);
}
function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
return value * (range_2_max / range_1_max);
return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
}
function randRange(...args) {
let min, max;
if (typeof args[0] === "number" && typeof args[1] === "number") {
[min, max] = args;
} else if (typeof args[0] === "number" && typeof args[1] !== "number") {
min = 0;
max = args[0];
} else
throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
min = Number(min);
max = Number(max);
if (isNaN(min) || isNaN(max))
throw new TypeError(`Parameters "min" and "max" can't be NaN`);
if (min > max)
throw new TypeError(`Parameter "min" can't be bigger than "max"`);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
// lib/array.ts
function randomItem(array) {
return randomItemIndex(array)[0];
}
function randomItemIndex(array) {
if (array.length === 0)
return [void 0, void 0];
const idx = randRange(array.length - 1);
return [array[idx], idx];
}
function takeRandomItem(arr) {
const [itm, idx] = randomItemIndex(arr);
if (idx === void 0)
return void 0;
arr.splice(idx, 1);
return itm;
}
function randomizeArray(array) {
const retArray = [...array];
if (array.length === 0)
return array;
for (let i = retArray.length - 1; i > 0; i--) {
const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
[retArray[i], retArray[j]] = [retArray[j], retArray[i]];
}
return retArray;
}
// lib/config.ts
var ConfigManager = class {
/**
* Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
* Supports migrating data from older versions of the configuration to newer ones and populating the cache with default data if no persistent data is found.
*
* ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
* ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
*
* @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `config.defaultConfig`) - this should also be the type of the data format associated with the current `options.formatVersion`
* @param options The options for this ConfigManager instance
*/
constructor(options) {
__publicField(this, "id");
__publicField(this, "formatVersion");
__publicField(this, "defaultConfig");
__publicField(this, "cachedConfig");
__publicField(this, "migrations");
this.id = options.id;
this.formatVersion = options.formatVersion;
this.defaultConfig = options.defaultConfig;
this.cachedConfig = options.defaultConfig;
this.migrations = options.migrations;
}
/**
* Loads the data saved in persistent storage into the in-memory cache and also returns it.
* Automatically populates persistent storage with default data if it doesn't contain any data yet.
* Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
*/
loadData() {
return __async(this, null, function* () {
try {
const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
if (typeof gmData !== "string") {
yield this.saveDefaultData();
return this.defaultConfig;
}
if (isNaN(gmFmtVer))
yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
let parsed = JSON.parse(gmData);
if (gmFmtVer < this.formatVersion && this.migrations)
parsed = yield this.runMigrations(parsed, gmFmtVer);
return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
} catch (err) {
yield this.saveDefaultData();
return this.defaultConfig;
}
});
}
/** Returns a copy of the data from the in-memory cache. Use `loadData()` to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage). */
getData() {
return this.deepCopy(this.cachedConfig);
}
/** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
setData(data) {
this.cachedConfig = data;
return new Promise((resolve) => __async(this, null, function* () {
yield Promise.all([
GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
]);
resolve();
}));
}
/** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
saveDefaultData() {
return __async(this, null, function* () {
this.cachedConfig = this.defaultConfig;
return new Promise((resolve) => __async(this, null, function* () {
yield Promise.all([
GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
]);
resolve();
}));
});
}
/**
* Call this method to clear all persistently stored data associated with this ConfigManager instance.
* The in-memory cache will be left untouched, so you may still access the data with `getData()`.
* Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.
*
* ⚠️ This requires the additional directive `@grant GM.deleteValue`
*/
deleteConfig() {
return __async(this, null, function* () {
yield Promise.all([
GM.deleteValue(`_uucfg-${this.id}`),
GM.deleteValue(`_uucfgver-${this.id}`)
]);
});
}
/** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
runMigrations(oldData, oldFmtVer) {
return __async(this, null, function* () {
if (!this.migrations)
return oldData;
let newData = oldData;
const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
let lastFmtVer = oldFmtVer;
for (const [fmtVer, migrationFunc] of sortedMigrations) {
const ver = Number(fmtVer);
if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
try {
const migRes = migrationFunc(newData);
newData = migRes instanceof Promise ? yield migRes : migRes;
lastFmtVer = oldFmtVer = ver;
} catch (err) {
console.error(`Error while running migration function for format version ${fmtVer}:`, err);
}
}
}
yield Promise.all([
GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
]);
return newData;
});
}
/** Copies a JSON-compatible object and loses its internal references */
deepCopy(obj) {
return JSON.parse(JSON.stringify(obj));
}
};
// lib/dom.ts
function getUnsafeWindow() {
try {
return unsafeWindow;
} catch (e) {
return window;
}
}
function insertAfter(beforeElement, afterElement) {
var _a;
(_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
return afterElement;
}
function addParent(element, newParent) {
const oldParent = element.parentNode;
if (!oldParent)
throw new Error("Element doesn't have a parent node");
oldParent.replaceChild(newParent, element);
newParent.appendChild(element);
return newParent;
}
function addGlobalStyle(style) {
const styleElem = document.createElement("style");
styleElem.innerHTML = style;
document.head.appendChild(styleElem);
}
function preloadImages(srcUrls, rejects = false) {
const promises = srcUrls.map((src) => new Promise((res, rej) => {
const image = new Image();
image.src = src;
image.addEventListener("load", () => res(image));
image.addEventListener("error", (evt) => rejects && rej(evt));
}));
return Promise.allSettled(promises);
}
function openInNewTab(href) {
const openElem = document.createElement("a");
Object.assign(openElem, {
className: "userutils-open-in-new-tab",
target: "_blank",
rel: "noopener noreferrer",
href
});
openElem.style.display = "none";
document.body.appendChild(openElem);
openElem.click();
setTimeout(openElem.remove, 50);
}
function interceptEvent(eventObject, eventName, predicate) {
if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
Error.stackTraceLimit = 1e3;
}
(function(original) {
eventObject.__proto__.addEventListener = function(...args) {
var _a, _b;
const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
args[1] = function(...a) {
if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
return;
else
return origListener.apply(this, a);
};
original.apply(this, args);
};
})(eventObject.__proto__.addEventListener);
}
function interceptWindowEvent(eventName, predicate) {
return interceptEvent(getUnsafeWindow(), eventName, predicate);
}
function amplifyMedia(mediaElement, initialMultiplier = 1) {
const context = new (window.AudioContext || window.webkitAudioContext)();
const props = {
/** Sets the gain multiplier */
setGain(multiplier) {
props.gainNode.gain.setValueAtTime(multiplier, props.context.currentTime);
},
/** Returns the current gain multiplier */
getGain() {
return props.gainNode.gain.value;
},
/** Enable the amplification for the first time or if it was disabled before */
enable() {
props.source.connect(props.limiterNode);
props.limiterNode.connect(props.gainNode);
props.gainNode.connect(props.context.destination);
},
/** Disable the amplification */
disable() {
props.source.disconnect(props.limiterNode);
props.limiterNode.disconnect(props.gainNode);
props.gainNode.disconnect(props.context.destination);
props.source.connect(props.context.destination);
},
/**
* Set the options of the [limiter / DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options)
* The default is `{ threshold: -2, knee: 40, ratio: 12, attack: 0.003, release: 0.25 }`
*/
setLimiterOptions(options) {
for (const [key, val] of Object.entries(options))
props.limiterNode[key].setValueAtTime(val, props.context.currentTime);
},
context,
source: context.createMediaElementSource(mediaElement),
gainNode: context.createGain(),
limiterNode: context.createDynamicsCompressor()
};
props.setLimiterOptions({
threshold: -2,
knee: 40,
ratio: 12,
attack: 3e-3,
release: 0.25
});
props.setGain(initialMultiplier);
return props;
}
function isScrollable(element) {
const { overflowX, overflowY } = getComputedStyle(element);
return {
vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
};
}
// lib/misc.ts
function autoPlural(word, num) {
if (Array.isArray(num) || num instanceof NodeList)
num = num.length;
return `${word}${num === 1 ? "" : "s"}`;
}
function pauseFor(time) {
return new Promise((res) => {
setTimeout(() => res(), time);
});
}
function debounce(func, timeout = 300) {
let timer;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => func.apply(this, args), timeout);
};
}
function fetchAdvanced(_0) {
return __async(this, arguments, function* (url, options = {}) {
const { timeout = 1e4 } = options;
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
signal: controller.signal
}));
clearTimeout(id);
return res;
});
}
function insertValues(str, ...values) {
return str.replace(/%\d/gm, (match) => {
var _a, _b;
const argIndex = Number(match.substring(1)) - 1;
return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
});
}
// lib/onSelector.ts
var selectorMap = /* @__PURE__ */ new Map();
function onSelector(selector, options) {
let selectorMapItems = [];
if (selectorMap.has(selector))
selectorMapItems = selectorMap.get(selector);
selectorMapItems.push(options);
selectorMap.set(selector, selectorMapItems);
checkSelectorExists(selector, selectorMapItems);
}
function removeOnSelector(selector) {
return selectorMap.delete(selector);
}
function checkSelectorExists(selector, options) {
const deleteIndices = [];
options.forEach((option, i) => {
try {
const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
option.listener(elements);
if (!option.continuous)
deleteIndices.push(i);
}
} catch (err) {
console.error(`Couldn't call listener for selector '${selector}'`, err);
}
});
if (deleteIndices.length > 0) {
const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
if (newOptsArray.length === 0)
selectorMap.delete(selector);
else {
selectorMap.set(selector, newOptsArray);
}
}
}
function initOnSelector(options = {}) {
const observer = new MutationObserver(() => {
for (const [selector, options2] of selectorMap.entries())
checkSelectorExists(selector, options2);
});
observer.observe(document.body, __spreadValues({
subtree: true,
childList: true
}, options));
}
function getSelectorMap() {
return selectorMap;
}
// lib/translation.ts
var trans = {};
var curLang;
function tr(key, ...args) {
var _a;
if (!curLang)
return key;
const trText = (_a = trans[curLang]) == null ? void 0 : _a[key];
if (!trText)
return key;
if (args.length > 0 && trText.match(/%\d/)) {
return insertValues(trText, ...args);
}
return trText;
}
tr.addLanguage = (language, translations) => {
trans[language] = translations;
};
tr.setLanguage = (language) => {
curLang = language;
};
tr.getLanguage = () => {
return curLang;
};
exports.ConfigManager = ConfigManager;
exports.addGlobalStyle = addGlobalStyle;
exports.addParent = addParent;
exports.amplifyMedia = amplifyMedia;
exports.autoPlural = autoPlural;
exports.clamp = clamp;
exports.debounce = debounce;
exports.fetchAdvanced = fetchAdvanced;
exports.getSelectorMap = getSelectorMap;
exports.getUnsafeWindow = getUnsafeWindow;
exports.initOnSelector = initOnSelector;
exports.insertAfter = insertAfter;
exports.insertValues = insertValues;
exports.interceptEvent = interceptEvent;
exports.interceptWindowEvent = interceptWindowEvent;
exports.isScrollable = isScrollable;
exports.mapRange = mapRange;
exports.onSelector = onSelector;
exports.openInNewTab = openInNewTab;
exports.pauseFor = pauseFor;
exports.preloadImages = preloadImages;
exports.randRange = randRange;
exports.randomItem = randomItem;
exports.randomItemIndex = randomItemIndex;
exports.randomizeArray = randomizeArray;
exports.removeOnSelector = removeOnSelector;
exports.takeRandomItem = takeRandomItem;
exports.tr = tr;
return exports;
})({});