UserUtils

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

当前为 2023-10-01 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/472956/1258798/UserUtils.js

  1. // ==UserScript==
  2. // @namespace https://github.com/Sv443-Network/UserUtils
  3. // @exclude *
  4. // @author Sv443
  5. // @supportURL https://github.com/Sv443-Network/UserUtils/issues
  6. // @homepageURL https://github.com/Sv443-Network/UserUtils#readme
  7. // @supportURL https://github.com/Sv443-Network/UserUtils/issues
  8.  
  9. // ==UserLibrary==
  10. // @name UserUtils
  11. // @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
  12. // @version 2.0.0
  13. // @license MIT
  14. // @copyright Sv443 (https://github.com/Sv443)
  15.  
  16. // ==/UserScript==
  17. // ==/UserLibrary==
  18.  
  19. // ==OpenUserJS==
  20. // @author Sv443
  21. // ==/OpenUserJS==
  22.  
  23. var UserUtils = (function (exports) {
  24. var __defProp = Object.defineProperty;
  25. var __defProps = Object.defineProperties;
  26. var __getOwnPropDescs = Object.getOwnPropertyDescriptors;
  27. var __getOwnPropSymbols = Object.getOwnPropertySymbols;
  28. var __hasOwnProp = Object.prototype.hasOwnProperty;
  29. var __propIsEnum = Object.prototype.propertyIsEnumerable;
  30. var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
  31. var __spreadValues = (a, b) => {
  32. for (var prop in b || (b = {}))
  33. if (__hasOwnProp.call(b, prop))
  34. __defNormalProp(a, prop, b[prop]);
  35. if (__getOwnPropSymbols)
  36. for (var prop of __getOwnPropSymbols(b)) {
  37. if (__propIsEnum.call(b, prop))
  38. __defNormalProp(a, prop, b[prop]);
  39. }
  40. return a;
  41. };
  42. var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
  43. var __publicField = (obj, key, value) => {
  44. __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
  45. return value;
  46. };
  47. var __async = (__this, __arguments, generator) => {
  48. return new Promise((resolve, reject) => {
  49. var fulfilled = (value) => {
  50. try {
  51. step(generator.next(value));
  52. } catch (e) {
  53. reject(e);
  54. }
  55. };
  56. var rejected = (value) => {
  57. try {
  58. step(generator.throw(value));
  59. } catch (e) {
  60. reject(e);
  61. }
  62. };
  63. var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
  64. step((generator = generator.apply(__this, __arguments)).next());
  65. });
  66. };
  67.  
  68. // lib/math.ts
  69. function clamp(value, min, max) {
  70. return Math.max(Math.min(value, max), min);
  71. }
  72. function mapRange(value, range_1_min, range_1_max, range_2_min, range_2_max) {
  73. if (Number(range_1_min) === 0 && Number(range_2_min) === 0)
  74. return value * (range_2_max / range_1_max);
  75. return (value - range_1_min) * ((range_2_max - range_2_min) / (range_1_max - range_1_min)) + range_2_min;
  76. }
  77. function randRange(...args) {
  78. let min, max;
  79. if (typeof args[0] === "number" && typeof args[1] === "number") {
  80. [min, max] = args;
  81. } else if (typeof args[0] === "number" && typeof args[1] !== "number") {
  82. min = 0;
  83. max = args[0];
  84. } else
  85. throw new TypeError(`Wrong parameter(s) provided - expected: "number" and "number|undefined", got: "${typeof args[0]}" and "${typeof args[1]}"`);
  86. min = Number(min);
  87. max = Number(max);
  88. if (isNaN(min) || isNaN(max))
  89. throw new TypeError(`Parameters "min" and "max" can't be NaN`);
  90. if (min > max)
  91. throw new TypeError(`Parameter "min" can't be bigger than "max"`);
  92. return Math.floor(Math.random() * (max - min + 1)) + min;
  93. }
  94.  
  95. // lib/array.ts
  96. function randomItem(array) {
  97. return randomItemIndex(array)[0];
  98. }
  99. function randomItemIndex(array) {
  100. if (array.length === 0)
  101. return [void 0, void 0];
  102. const idx = randRange(array.length - 1);
  103. return [array[idx], idx];
  104. }
  105. function takeRandomItem(arr) {
  106. const [itm, idx] = randomItemIndex(arr);
  107. if (idx === void 0)
  108. return void 0;
  109. arr.splice(idx, 1);
  110. return itm;
  111. }
  112. function randomizeArray(array) {
  113. const retArray = [...array];
  114. if (array.length === 0)
  115. return array;
  116. for (let i = retArray.length - 1; i > 0; i--) {
  117. const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
  118. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  119. }
  120. return retArray;
  121. }
  122.  
  123. // lib/config.ts
  124. var ConfigManager = class {
  125. /**
  126. * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
  127. * 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.
  128. *
  129. * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
  130. * ⚠️ Make sure to call `loadData()` at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
  131. *
  132. * @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`
  133. * @param options The options for this ConfigManager instance
  134. */
  135. constructor(options) {
  136. __publicField(this, "id");
  137. __publicField(this, "formatVersion");
  138. __publicField(this, "defaultConfig");
  139. __publicField(this, "cachedConfig");
  140. __publicField(this, "migrations");
  141. this.id = options.id;
  142. this.formatVersion = options.formatVersion;
  143. this.defaultConfig = options.defaultConfig;
  144. this.cachedConfig = options.defaultConfig;
  145. this.migrations = options.migrations;
  146. }
  147. /**
  148. * Loads the data saved in persistent storage into the in-memory cache and also returns it.
  149. * Automatically populates persistent storage with default data if it doesn't contain any data yet.
  150. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
  151. */
  152. loadData() {
  153. return __async(this, null, function* () {
  154. try {
  155. const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
  156. let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
  157. if (typeof gmData !== "string") {
  158. yield this.saveDefaultData();
  159. return this.defaultConfig;
  160. }
  161. if (isNaN(gmFmtVer))
  162. yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
  163. let parsed = JSON.parse(gmData);
  164. if (gmFmtVer < this.formatVersion && this.migrations)
  165. parsed = yield this.runMigrations(parsed, gmFmtVer);
  166. return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
  167. } catch (err) {
  168. yield this.saveDefaultData();
  169. return this.defaultConfig;
  170. }
  171. });
  172. }
  173. /** 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). */
  174. getData() {
  175. return this.deepCopy(this.cachedConfig);
  176. }
  177. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  178. setData(data) {
  179. this.cachedConfig = data;
  180. return new Promise((resolve) => __async(this, null, function* () {
  181. yield Promise.all([
  182. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
  183. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  184. ]);
  185. resolve();
  186. }));
  187. }
  188. /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
  189. saveDefaultData() {
  190. return __async(this, null, function* () {
  191. this.cachedConfig = this.defaultConfig;
  192. return new Promise((resolve) => __async(this, null, function* () {
  193. yield Promise.all([
  194. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
  195. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  196. ]);
  197. resolve();
  198. }));
  199. });
  200. }
  201. /**
  202. * Call this method to clear all persistently stored data associated with this ConfigManager instance.
  203. * The in-memory cache will be left untouched, so you may still access the data with `getData()`.
  204. * Calling `loadData()` or `setData()` after this method was called will recreate persistent storage with the cached or default data.
  205. *
  206. * ⚠️ This requires the additional directive `@grant GM.deleteValue`
  207. */
  208. deleteConfig() {
  209. return __async(this, null, function* () {
  210. yield Promise.all([
  211. GM.deleteValue(`_uucfg-${this.id}`),
  212. GM.deleteValue(`_uucfgver-${this.id}`)
  213. ]);
  214. });
  215. }
  216. /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
  217. runMigrations(oldData, oldFmtVer) {
  218. return __async(this, null, function* () {
  219. if (!this.migrations)
  220. return oldData;
  221. let newData = oldData;
  222. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  223. let lastFmtVer = oldFmtVer;
  224. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  225. const ver = Number(fmtVer);
  226. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  227. try {
  228. const migRes = migrationFunc(newData);
  229. newData = migRes instanceof Promise ? yield migRes : migRes;
  230. lastFmtVer = oldFmtVer = ver;
  231. } catch (err) {
  232. console.error(`Error while running migration function for format version ${fmtVer}:`, err);
  233. }
  234. }
  235. }
  236. yield Promise.all([
  237. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
  238. GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
  239. ]);
  240. return newData;
  241. });
  242. }
  243. /** Copies a JSON-compatible object and loses its internal references */
  244. deepCopy(obj) {
  245. return JSON.parse(JSON.stringify(obj));
  246. }
  247. };
  248.  
  249. // lib/dom.ts
  250. function getUnsafeWindow() {
  251. try {
  252. return unsafeWindow;
  253. } catch (e) {
  254. return window;
  255. }
  256. }
  257. function insertAfter(beforeElement, afterElement) {
  258. var _a;
  259. (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
  260. return afterElement;
  261. }
  262. function addParent(element, newParent) {
  263. const oldParent = element.parentNode;
  264. if (!oldParent)
  265. throw new Error("Element doesn't have a parent node");
  266. oldParent.replaceChild(newParent, element);
  267. newParent.appendChild(element);
  268. return newParent;
  269. }
  270. function addGlobalStyle(style) {
  271. const styleElem = document.createElement("style");
  272. styleElem.innerHTML = style;
  273. document.head.appendChild(styleElem);
  274. }
  275. function preloadImages(srcUrls, rejects = false) {
  276. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  277. const image = new Image();
  278. image.src = src;
  279. image.addEventListener("load", () => res(image));
  280. image.addEventListener("error", (evt) => rejects && rej(evt));
  281. }));
  282. return Promise.allSettled(promises);
  283. }
  284. function openInNewTab(href) {
  285. const openElem = document.createElement("a");
  286. Object.assign(openElem, {
  287. className: "userutils-open-in-new-tab",
  288. target: "_blank",
  289. rel: "noopener noreferrer",
  290. href
  291. });
  292. openElem.style.display = "none";
  293. document.body.appendChild(openElem);
  294. openElem.click();
  295. setTimeout(openElem.remove, 50);
  296. }
  297. function interceptEvent(eventObject, eventName, predicate) {
  298. if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
  299. Error.stackTraceLimit = 1e3;
  300. }
  301. (function(original) {
  302. eventObject.__proto__.addEventListener = function(...args) {
  303. var _a, _b;
  304. const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
  305. args[1] = function(...a) {
  306. if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
  307. return;
  308. else
  309. return origListener.apply(this, a);
  310. };
  311. original.apply(this, args);
  312. };
  313. })(eventObject.__proto__.addEventListener);
  314. }
  315. function interceptWindowEvent(eventName, predicate) {
  316. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  317. }
  318. function amplifyMedia(mediaElement, initialMultiplier = 1) {
  319. const context = new (window.AudioContext || window.webkitAudioContext)();
  320. const props = {
  321. /** Sets the gain multiplier */
  322. setGain(multiplier) {
  323. props.gainNode.gain.setValueAtTime(multiplier, props.context.currentTime);
  324. },
  325. /** Returns the current gain multiplier */
  326. getGain() {
  327. return props.gainNode.gain.value;
  328. },
  329. /** Enable the amplification for the first time or if it was disabled before */
  330. enable() {
  331. props.source.connect(props.limiterNode);
  332. props.limiterNode.connect(props.gainNode);
  333. props.gainNode.connect(props.context.destination);
  334. },
  335. /** Disable the amplification */
  336. disable() {
  337. props.source.disconnect(props.limiterNode);
  338. props.limiterNode.disconnect(props.gainNode);
  339. props.gainNode.disconnect(props.context.destination);
  340. props.source.connect(props.context.destination);
  341. },
  342. /**
  343. * Set the options of the [limiter / DynamicsCompressorNode](https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode/DynamicsCompressorNode#options)
  344. * The default is `{ threshold: -2, knee: 40, ratio: 12, attack: 0.003, release: 0.25 }`
  345. */
  346. setLimiterOptions(options) {
  347. for (const [key, val] of Object.entries(options))
  348. props.limiterNode[key].setValueAtTime(val, props.context.currentTime);
  349. },
  350. context,
  351. source: context.createMediaElementSource(mediaElement),
  352. gainNode: context.createGain(),
  353. limiterNode: context.createDynamicsCompressor()
  354. };
  355. props.setLimiterOptions({
  356. threshold: -2,
  357. knee: 40,
  358. ratio: 12,
  359. attack: 3e-3,
  360. release: 0.25
  361. });
  362. props.setGain(initialMultiplier);
  363. return props;
  364. }
  365. function isScrollable(element) {
  366. const { overflowX, overflowY } = getComputedStyle(element);
  367. return {
  368. vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
  369. horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
  370. };
  371. }
  372.  
  373. // lib/misc.ts
  374. function autoPlural(word, num) {
  375. if (Array.isArray(num) || num instanceof NodeList)
  376. num = num.length;
  377. return `${word}${num === 1 ? "" : "s"}`;
  378. }
  379. function pauseFor(time) {
  380. return new Promise((res) => {
  381. setTimeout(() => res(), time);
  382. });
  383. }
  384. function debounce(func, timeout = 300) {
  385. let timer;
  386. return function(...args) {
  387. clearTimeout(timer);
  388. timer = setTimeout(() => func.apply(this, args), timeout);
  389. };
  390. }
  391. function fetchAdvanced(_0) {
  392. return __async(this, arguments, function* (url, options = {}) {
  393. const { timeout = 1e4 } = options;
  394. const controller = new AbortController();
  395. const id = setTimeout(() => controller.abort(), timeout);
  396. const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
  397. signal: controller.signal
  398. }));
  399. clearTimeout(id);
  400. return res;
  401. });
  402. }
  403. function insertValues(str, ...values) {
  404. return str.replace(/%\d/gm, (match) => {
  405. var _a, _b;
  406. const argIndex = Number(match.substring(1)) - 1;
  407. return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
  408. });
  409. }
  410.  
  411. // lib/onSelector.ts
  412. var selectorMap = /* @__PURE__ */ new Map();
  413. function onSelector(selector, options) {
  414. let selectorMapItems = [];
  415. if (selectorMap.has(selector))
  416. selectorMapItems = selectorMap.get(selector);
  417. selectorMapItems.push(options);
  418. selectorMap.set(selector, selectorMapItems);
  419. checkSelectorExists(selector, selectorMapItems);
  420. }
  421. function removeOnSelector(selector) {
  422. return selectorMap.delete(selector);
  423. }
  424. function checkSelectorExists(selector, options) {
  425. const deleteIndices = [];
  426. options.forEach((option, i) => {
  427. try {
  428. const elements = option.all ? document.querySelectorAll(selector) : document.querySelector(selector);
  429. if (elements !== null && elements instanceof NodeList && elements.length > 0 || elements !== null) {
  430. option.listener(elements);
  431. if (!option.continuous)
  432. deleteIndices.push(i);
  433. }
  434. } catch (err) {
  435. console.error(`Couldn't call listener for selector '${selector}'`, err);
  436. }
  437. });
  438. if (deleteIndices.length > 0) {
  439. const newOptsArray = options.filter((_, i) => !deleteIndices.includes(i));
  440. if (newOptsArray.length === 0)
  441. selectorMap.delete(selector);
  442. else {
  443. selectorMap.set(selector, newOptsArray);
  444. }
  445. }
  446. }
  447. function initOnSelector(options = {}) {
  448. const observer = new MutationObserver(() => {
  449. for (const [selector, options2] of selectorMap.entries())
  450. checkSelectorExists(selector, options2);
  451. });
  452. observer.observe(document.body, __spreadValues({
  453. subtree: true,
  454. childList: true
  455. }, options));
  456. }
  457. function getSelectorMap() {
  458. return selectorMap;
  459. }
  460.  
  461. // lib/translation.ts
  462. var trans = {};
  463. var curLang;
  464. function tr(key, ...args) {
  465. var _a;
  466. if (!curLang)
  467. return key;
  468. const trText = (_a = trans[curLang]) == null ? void 0 : _a[key];
  469. if (!trText)
  470. return key;
  471. if (args.length > 0 && trText.match(/%\d/)) {
  472. return insertValues(trText, ...args);
  473. }
  474. return trText;
  475. }
  476. tr.addLanguage = (language, translations) => {
  477. trans[language] = translations;
  478. };
  479. tr.setLanguage = (language) => {
  480. curLang = language;
  481. };
  482. tr.getLanguage = () => {
  483. return curLang;
  484. };
  485.  
  486. exports.ConfigManager = ConfigManager;
  487. exports.addGlobalStyle = addGlobalStyle;
  488. exports.addParent = addParent;
  489. exports.amplifyMedia = amplifyMedia;
  490. exports.autoPlural = autoPlural;
  491. exports.clamp = clamp;
  492. exports.debounce = debounce;
  493. exports.fetchAdvanced = fetchAdvanced;
  494. exports.getSelectorMap = getSelectorMap;
  495. exports.getUnsafeWindow = getUnsafeWindow;
  496. exports.initOnSelector = initOnSelector;
  497. exports.insertAfter = insertAfter;
  498. exports.insertValues = insertValues;
  499. exports.interceptEvent = interceptEvent;
  500. exports.interceptWindowEvent = interceptWindowEvent;
  501. exports.isScrollable = isScrollable;
  502. exports.mapRange = mapRange;
  503. exports.onSelector = onSelector;
  504. exports.openInNewTab = openInNewTab;
  505. exports.pauseFor = pauseFor;
  506. exports.preloadImages = preloadImages;
  507. exports.randRange = randRange;
  508. exports.randomItem = randomItem;
  509. exports.randomItemIndex = randomItemIndex;
  510. exports.randomizeArray = randomizeArray;
  511. exports.removeOnSelector = removeOnSelector;
  512. exports.takeRandomItem = takeRandomItem;
  513. exports.tr = tr;
  514.  
  515. return exports;
  516.  
  517. })({});

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址