您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.greasyforks.org/scripts/472956/1348027/UserUtils.js
// ==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 // ==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 6.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 __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 __objRest = (source, exclude) => { var target = {}; for (var prop in source) if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0) target[prop] = source[prop]; if (source != null && __getOwnPropSymbols) for (var prop of __getOwnPropSymbols(source)) { if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop)) target[prop] = source[prop]; } return target; }; 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, range1min, range1max, range2min, range2max) { if (Number(range1min) === 0 && Number(range2min) === 0) return value * (range2max / range1max); return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min; } 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; } 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)) return NaN; if (min > max) throw new TypeError(`Parameter "min" can't be bigger than "max"`); return Math.floor(Math.random() * (max - min + 1)) + min; } function randomId(length = 16, radix = 16) { const arr = new Uint8Array(length); crypto.getRandomValues(arr); return Array.from( arr, (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1) ).join(""); } // 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 retArray; 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/DataStore.ts var DataStore = class { /** * Creates an instance of DataStore to manage a sync & async database that is cached in memory and persistently saved across sessions. * Supports migrating data from older versions 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 {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultData` * * @template TData The type of the data that is saved in persistent storage (will be automatically inferred from `config.defaultData`) - this should also be the type of the data format associated with the current `options.formatVersion` * @param options The options for this DataStore instance */ constructor(options) { __publicField(this, "id"); __publicField(this, "formatVersion"); __publicField(this, "defaultData"); __publicField(this, "cachedData"); __publicField(this, "migrations"); __publicField(this, "encodeData"); __publicField(this, "decodeData"); this.id = options.id; this.formatVersion = options.formatVersion; this.defaultData = options.defaultData; this.cachedData = options.defaultData; this.migrations = options.migrations; this.encodeData = options.encodeData; this.decodeData = options.decodeData; } /** * 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.defaultData); let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`)); if (typeof gmData !== "string") { yield this.saveDefaultData(); return __spreadValues({}, this.defaultData); } const isEncoded = yield GM.getValue(`_uucfgenc-${this.id}`, false); if (isNaN(gmFmtVer)) yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion); let parsed = yield this.deserializeData(gmData, isEncoded); if (gmFmtVer < this.formatVersion && this.migrations) parsed = yield this.runMigrations(parsed, gmFmtVer); return __spreadValues({}, this.cachedData = parsed); } catch (err) { console.warn("Error while parsing JSON data, resetting it to the default value.", err); yield this.saveDefaultData(); return this.defaultData; } }); } /** * Returns a copy of the data from the in-memory cache. * Use {@linkcode 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.cachedData); } /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */ setData(data) { this.cachedData = data; const useEncoding = Boolean(this.encodeData && this.decodeData); return new Promise((resolve) => __async(this, null, function* () { yield Promise.all([ GM.setValue(`_uucfg-${this.id}`, yield this.serializeData(data, useEncoding)), GM.setValue(`_uucfgver-${this.id}`, this.formatVersion), GM.setValue(`_uucfgenc-${this.id}`, useEncoding) ]); 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.cachedData = this.defaultData; const useEncoding = Boolean(this.encodeData && this.decodeData); return new Promise((resolve) => __async(this, null, function* () { yield Promise.all([ GM.setValue(`_uucfg-${this.id}`, yield this.serializeData(this.defaultData, useEncoding)), GM.setValue(`_uucfgver-${this.id}`, this.formatVersion), GM.setValue(`_uucfgenc-${this.id}`, useEncoding) ]); resolve(); })); }); } /** * Call this method to clear all persistently stored data associated with this DataStore instance. * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()} * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data. * * ⚠️ This requires the additional directive `@grant GM.deleteValue` */ deleteData() { return __async(this, null, function* () { yield Promise.all([ GM.deleteValue(`_uucfg-${this.id}`), GM.deleteValue(`_uucfgver-${this.id}`), GM.deleteValue(`_uucfgenc-${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}' - resetting to the default value.`, err); yield this.saveDefaultData(); return this.getData(); } } } yield Promise.all([ GM.setValue(`_uucfg-${this.id}`, yield this.serializeData(newData)), GM.setValue(`_uucfgver-${this.id}`, lastFmtVer), GM.setValue(`_uucfgenc-${this.id}`, Boolean(this.encodeData && this.decodeData)) ]); return newData; }); } /** Serializes the data using the optional this.encodeData() and returns it as a string */ serializeData(data, useEncoding = true) { return __async(this, null, function* () { const stringData = JSON.stringify(data); if (!this.encodeData || !this.decodeData || !useEncoding) return stringData; const encRes = this.encodeData(stringData); if (encRes instanceof Promise) return yield encRes; return encRes; }); } /** Deserializes the data using the optional this.decodeData() and returns it as a JSON object */ deserializeData(data, useEncoding = true) { return __async(this, null, function* () { let decRes = this.decodeData && this.encodeData && useEncoding ? this.decodeData(data) : void 0; if (decRes instanceof Promise) decRes = yield decRes; return JSON.parse(decRes != null ? decRes : data); }); } /** 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); return 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 = () => true) { Error.stackTraceLimit = Math.max(Error.stackTraceLimit, 100); if (isNaN(Error.stackTraceLimit)) Error.stackTraceLimit = 100; (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 = () => true) { return interceptEvent(getUnsafeWindow(), eventName, predicate); } 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 }; } function observeElementProp(element, property, callback) { const elementPrototype = Object.getPrototypeOf(element); if (elementPrototype.hasOwnProperty(property)) { const descriptor = Object.getOwnPropertyDescriptor(elementPrototype, property); Object.defineProperty(element, property, { get: function() { var _a; return (_a = descriptor == null ? void 0 : descriptor.get) == null ? void 0 : _a.apply(this, arguments); }, set: function() { var _a; const oldValue = this[property]; (_a = descriptor == null ? void 0 : descriptor.set) == null ? void 0 : _a.apply(this, arguments); const newValue = this[property]; if (typeof callback === "function") { callback.bind(this, oldValue, newValue); } return newValue; } }); } } function getSiblingsFrame(refElement, siblingAmount, refElementAlignment = "center-top", includeRef = true) { var _a, _b; const siblings = [...(_b = (_a = refElement.parentNode) == null ? void 0 : _a.childNodes) != null ? _b : []]; const elemSiblIdx = siblings.indexOf(refElement); if (elemSiblIdx === -1) throw new Error("Element doesn't have a parent node"); if (refElementAlignment === "top") return [...siblings.slice(elemSiblIdx + Number(!includeRef), elemSiblIdx + siblingAmount + Number(!includeRef))]; else if (refElementAlignment.startsWith("center-")) { const halfAmount = (refElementAlignment === "center-bottom" ? Math.ceil : Math.floor)(siblingAmount / 2); const startIdx = Math.max(0, elemSiblIdx - halfAmount); const topOffset = Number(refElementAlignment === "center-top" && siblingAmount % 2 === 0 && includeRef); const btmOffset = Number(refElementAlignment === "center-bottom" && siblingAmount % 2 !== 0 && includeRef); const startIdxWithOffset = startIdx + topOffset + btmOffset; return [ ...siblings.filter((_, idx) => includeRef || idx !== elemSiblIdx).slice(startIdxWithOffset, startIdxWithOffset + siblingAmount) ]; } else if (refElementAlignment === "bottom") return [...siblings.slice(elemSiblIdx - siblingAmount + Number(includeRef), elemSiblIdx + Number(includeRef))]; return []; } // 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* (input, options = {}) { const { timeout = 1e4 } = options; let signalOpts = {}, id = void 0; if (timeout >= 0) { const controller = new AbortController(); id = setTimeout(() => controller.abort(), timeout); signalOpts = { signal: controller.signal }; } const res = yield fetch(input, __spreadValues(__spreadValues({}, options), signalOpts)); clearTimeout(id); return res; }); } function insertValues(input, ...values) { return input.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(); }); } function compress(input, compressionFormat, outputType = "string") { return __async(this, null, function* () { const byteArray = typeof input === "string" ? new TextEncoder().encode(input) : input; const comp = new CompressionStream(compressionFormat); const writer = comp.writable.getWriter(); writer.write(byteArray); writer.close(); const buf = yield new Response(comp.readable).arrayBuffer(); return outputType === "arrayBuffer" ? buf : ab2str(buf); }); } function decompress(input, compressionFormat, outputType = "string") { return __async(this, null, function* () { const byteArray = typeof input === "string" ? str2ab(input) : input; const decomp = new DecompressionStream(compressionFormat); const writer = decomp.writable.getWriter(); writer.write(byteArray); writer.close(); const buf = yield new Response(decomp.readable).arrayBuffer(); return outputType === "arrayBuffer" ? buf : new TextDecoder().decode(buf); }); } function ab2str(buf) { return getUnsafeWindow().btoa( new Uint8Array(buf).reduce((data, byte) => data + String.fromCharCode(byte), "") ); } function str2ab(str) { return Uint8Array.from(getUnsafeWindow().atob(str), (c) => c.charCodeAt(0)); } // lib/SelectorObserver.ts var SelectorObserver = class { constructor(baseElement, options = {}) { __publicField(this, "enabled", false); __publicField(this, "baseElement"); __publicField(this, "observer"); __publicField(this, "observerOptions"); __publicField(this, "customOptions"); __publicField(this, "listenerMap"); this.baseElement = baseElement; this.listenerMap = /* @__PURE__ */ new Map(); this.observer = new MutationObserver(() => this.checkAllSelectors()); const _a = options, { defaultDebounce, disableOnNoListeners, enableOnAddListener } = _a, observerOptions = __objRest(_a, [ "defaultDebounce", "disableOnNoListeners", "enableOnAddListener" ]); this.observerOptions = __spreadValues({ childList: true, subtree: true }, observerOptions); this.customOptions = { defaultDebounce: defaultDebounce != null ? defaultDebounce : 0, disableOnNoListeners: disableOnNoListeners != null ? disableOnNoListeners : false, enableOnAddListener: enableOnAddListener != null ? enableOnAddListener : true }; } checkAllSelectors() { for (const [selector, listeners] of this.listenerMap.entries()) this.checkSelector(selector, listeners); } checkSelector(selector, listeners) { var _a; if (!this.enabled) return; const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement; if (!baseElement) return; const all = listeners.some((listener) => listener.all); const one = listeners.some((listener) => !listener.all); const allElements = all ? baseElement.querySelectorAll(selector) : null; const oneElement = one ? baseElement.querySelector(selector) : null; for (const options of listeners) { if (options.all) { if (allElements && allElements.length > 0) { options.listener(allElements); if (!options.continuous) this.removeListener(selector, options); } } else { if (oneElement) { options.listener(oneElement); if (!options.continuous) this.removeListener(selector, options); } } if (((_a = this.listenerMap.get(selector)) == null ? void 0 : _a.length) === 0) this.listenerMap.delete(selector); if (this.listenerMap.size === 0 && this.customOptions.disableOnNoListeners) this.disable(); } } debounce(func, time) { let timeout; return function(...args) { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), time); }; } /** * Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options} * @param selector The selector to observe * @param options Options for the selector observation * @param options.listener Gets called whenever the selector was found in the DOM * @param [options.all] Whether to use `querySelectorAll()` instead - default is false * @param [options.continuous] Whether to call the listener continuously instead of just once - default is false * @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default) */ addListener(selector, options) { options = __spreadValues({ all: false, continuous: false, debounce: 0 }, options); if (options.debounce && options.debounce > 0 || this.customOptions.defaultDebounce && this.customOptions.defaultDebounce > 0) { options.listener = this.debounce( options.listener, options.debounce || this.customOptions.defaultDebounce ); } if (this.listenerMap.has(selector)) this.listenerMap.get(selector).push(options); else this.listenerMap.set(selector, [options]); if (this.enabled === false && this.customOptions.enableOnAddListener) this.enable(); this.checkSelector(selector, [options]); } /** Disables the observation of the child elements */ disable() { if (!this.enabled) return; this.enabled = false; this.observer.disconnect(); } /** * Enables or reenables the observation of the child elements. * @param immediatelyCheckSelectors Whether to immediately check if all previously registered selectors exist (default is true) * @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found) */ enable(immediatelyCheckSelectors = true) { const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement; if (this.enabled || !baseElement) return false; this.enabled = true; this.observer.observe(baseElement, this.observerOptions); if (immediatelyCheckSelectors) this.checkAllSelectors(); return true; } /** Returns whether the observation of the child elements is currently enabled */ isEnabled() { return this.enabled; } /** Removes all listeners that have been registered with {@linkcode addListener()} */ clearListeners() { this.listenerMap.clear(); } /** * Removes all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} * @returns Returns true when all listeners for the associated selector were found and removed, false otherwise */ removeAllListeners(selector) { return this.listenerMap.delete(selector); } /** * Removes a single listener for the given {@linkcode selector} and {@linkcode options} that has been registered with {@linkcode addListener()} * @returns Returns true when the listener was found and removed, false otherwise */ removeListener(selector, options) { const listeners = this.listenerMap.get(selector); if (!listeners) return false; const index = listeners.indexOf(options); if (index > -1) { listeners.splice(index, 1); return true; } return false; } /** Returns all listeners that have been registered with {@linkcode addListener()} */ getAllListeners() { return this.listenerMap; } /** Returns all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} */ getListeners(selector) { return this.listenerMap.get(selector); } }; // 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.DataStore = DataStore; exports.SelectorObserver = SelectorObserver; exports.addGlobalStyle = addGlobalStyle; exports.addParent = addParent; exports.autoPlural = autoPlural; exports.clamp = clamp; exports.compress = compress; exports.debounce = debounce; exports.decompress = decompress; exports.fetchAdvanced = fetchAdvanced; exports.getSiblingsFrame = getSiblingsFrame; exports.getUnsafeWindow = getUnsafeWindow; exports.insertAfter = insertAfter; exports.insertValues = insertValues; exports.interceptEvent = interceptEvent; exports.interceptWindowEvent = interceptWindowEvent; exports.isScrollable = isScrollable; exports.mapRange = mapRange; exports.observeElementProp = observeElementProp; exports.openInNewTab = openInNewTab; exports.pauseFor = pauseFor; exports.preloadImages = preloadImages; exports.randRange = randRange; exports.randomId = randomId; exports.randomItem = randomItem; exports.randomItemIndex = randomItemIndex; exports.randomizeArray = randomizeArray; exports.takeRandomItem = takeRandomItem; exports.tr = tr; return exports; })({});