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-11-12 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/472956/1279362/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 3.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, range1min, range1max, range2min, range2max) {
  73. if (Number(range1min) === 0 && Number(range2min) === 0)
  74. return value * (range2max / range1max);
  75. return (value - range1min) * ((range2max - range2min) / (range1max - range1min)) + range2min;
  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;
  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. return 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. function randomId(length = 16, radix = 16) {
  95. const arr = new Uint8Array(length);
  96. crypto.getRandomValues(arr);
  97. return Array.from(
  98. arr,
  99. (v) => mapRange(v, 0, 255, 0, radix).toString(radix).substring(0, 1)
  100. ).join("");
  101. }
  102.  
  103. // lib/array.ts
  104. function randomItem(array) {
  105. return randomItemIndex(array)[0];
  106. }
  107. function randomItemIndex(array) {
  108. if (array.length === 0)
  109. return [void 0, void 0];
  110. const idx = randRange(array.length - 1);
  111. return [array[idx], idx];
  112. }
  113. function takeRandomItem(arr) {
  114. const [itm, idx] = randomItemIndex(arr);
  115. if (idx === void 0)
  116. return void 0;
  117. arr.splice(idx, 1);
  118. return itm;
  119. }
  120. function randomizeArray(array) {
  121. const retArray = [...array];
  122. if (array.length === 0)
  123. return array;
  124. for (let i = retArray.length - 1; i > 0; i--) {
  125. const j = Math.floor(randRange(0, 1e4) / 1e4 * (i + 1));
  126. [retArray[i], retArray[j]] = [retArray[j], retArray[i]];
  127. }
  128. return retArray;
  129. }
  130.  
  131. // lib/ConfigManager.ts
  132. var ConfigManager = class {
  133. /**
  134. * Creates an instance of ConfigManager to manage a user configuration that is cached in memory and persistently saved across sessions.
  135. * 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.
  136. *
  137. * ⚠️ Requires the directives `@grant GM.getValue` and `@grant GM.setValue`
  138. * ⚠️ Make sure to call {@linkcode loadData()} at least once after creating an instance, or the returned data will be the same as `options.defaultConfig`
  139. *
  140. * @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`
  141. * @param options The options for this ConfigManager instance
  142. */
  143. constructor(options) {
  144. __publicField(this, "id");
  145. __publicField(this, "formatVersion");
  146. __publicField(this, "defaultConfig");
  147. __publicField(this, "cachedConfig");
  148. __publicField(this, "migrations");
  149. this.id = options.id;
  150. this.formatVersion = options.formatVersion;
  151. this.defaultConfig = options.defaultConfig;
  152. this.cachedConfig = options.defaultConfig;
  153. this.migrations = options.migrations;
  154. }
  155. /**
  156. * Loads the data saved in persistent storage into the in-memory cache and also returns it.
  157. * Automatically populates persistent storage with default data if it doesn't contain any data yet.
  158. * Also runs all necessary migration functions if the data format has changed since the last time the data was saved.
  159. */
  160. loadData() {
  161. return __async(this, null, function* () {
  162. try {
  163. const gmData = yield GM.getValue(`_uucfg-${this.id}`, this.defaultConfig);
  164. let gmFmtVer = Number(yield GM.getValue(`_uucfgver-${this.id}`));
  165. if (typeof gmData !== "string") {
  166. yield this.saveDefaultData();
  167. return this.defaultConfig;
  168. }
  169. if (isNaN(gmFmtVer))
  170. yield GM.setValue(`_uucfgver-${this.id}`, gmFmtVer = this.formatVersion);
  171. let parsed = JSON.parse(gmData);
  172. if (gmFmtVer < this.formatVersion && this.migrations)
  173. parsed = yield this.runMigrations(parsed, gmFmtVer);
  174. return this.cachedConfig = typeof parsed === "object" ? parsed : void 0;
  175. } catch (err) {
  176. yield this.saveDefaultData();
  177. return this.defaultConfig;
  178. }
  179. });
  180. }
  181. /**
  182. * Returns a copy of the data from the in-memory cache.
  183. * Use {@linkcode loadData()} to get fresh data from persistent storage (usually not necessary since the cache should always exactly reflect persistent storage).
  184. */
  185. getData() {
  186. return this.deepCopy(this.cachedConfig);
  187. }
  188. /** Saves the data synchronously to the in-memory cache and asynchronously to the persistent storage */
  189. setData(data) {
  190. this.cachedConfig = data;
  191. return new Promise((resolve) => __async(this, null, function* () {
  192. yield Promise.all([
  193. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(data)),
  194. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  195. ]);
  196. resolve();
  197. }));
  198. }
  199. /** Saves the default configuration data passed in the constructor synchronously to the in-memory cache and asynchronously to persistent storage */
  200. saveDefaultData() {
  201. return __async(this, null, function* () {
  202. this.cachedConfig = this.defaultConfig;
  203. return new Promise((resolve) => __async(this, null, function* () {
  204. yield Promise.all([
  205. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(this.defaultConfig)),
  206. GM.setValue(`_uucfgver-${this.id}`, this.formatVersion)
  207. ]);
  208. resolve();
  209. }));
  210. });
  211. }
  212. /**
  213. * Call this method to clear all persistently stored data associated with this ConfigManager instance.
  214. * The in-memory cache will be left untouched, so you may still access the data with {@linkcode getData()}
  215. * Calling {@linkcode loadData()} or {@linkcode setData()} after this method was called will recreate persistent storage with the cached or default data.
  216. *
  217. * ⚠️ This requires the additional directive `@grant GM.deleteValue`
  218. */
  219. deleteConfig() {
  220. return __async(this, null, function* () {
  221. yield Promise.all([
  222. GM.deleteValue(`_uucfg-${this.id}`),
  223. GM.deleteValue(`_uucfgver-${this.id}`)
  224. ]);
  225. });
  226. }
  227. /** Runs all necessary migration functions consecutively - may be overwritten in a subclass */
  228. runMigrations(oldData, oldFmtVer) {
  229. return __async(this, null, function* () {
  230. if (!this.migrations)
  231. return oldData;
  232. let newData = oldData;
  233. const sortedMigrations = Object.entries(this.migrations).sort(([a], [b]) => Number(a) - Number(b));
  234. let lastFmtVer = oldFmtVer;
  235. for (const [fmtVer, migrationFunc] of sortedMigrations) {
  236. const ver = Number(fmtVer);
  237. if (oldFmtVer < this.formatVersion && oldFmtVer < ver) {
  238. try {
  239. const migRes = migrationFunc(newData);
  240. newData = migRes instanceof Promise ? yield migRes : migRes;
  241. lastFmtVer = oldFmtVer = ver;
  242. } catch (err) {
  243. console.error(`Error while running migration function for format version ${fmtVer}:`, err);
  244. }
  245. }
  246. }
  247. yield Promise.all([
  248. GM.setValue(`_uucfg-${this.id}`, JSON.stringify(newData)),
  249. GM.setValue(`_uucfgver-${this.id}`, lastFmtVer)
  250. ]);
  251. return newData;
  252. });
  253. }
  254. /** Copies a JSON-compatible object and loses its internal references */
  255. deepCopy(obj) {
  256. return JSON.parse(JSON.stringify(obj));
  257. }
  258. };
  259.  
  260. // lib/dom.ts
  261. function getUnsafeWindow() {
  262. try {
  263. return unsafeWindow;
  264. } catch (e) {
  265. return window;
  266. }
  267. }
  268. function insertAfter(beforeElement, afterElement) {
  269. var _a;
  270. (_a = beforeElement.parentNode) == null ? void 0 : _a.insertBefore(afterElement, beforeElement.nextSibling);
  271. return afterElement;
  272. }
  273. function addParent(element, newParent) {
  274. const oldParent = element.parentNode;
  275. if (!oldParent)
  276. throw new Error("Element doesn't have a parent node");
  277. oldParent.replaceChild(newParent, element);
  278. newParent.appendChild(element);
  279. return newParent;
  280. }
  281. function addGlobalStyle(style) {
  282. const styleElem = document.createElement("style");
  283. styleElem.innerHTML = style;
  284. document.head.appendChild(styleElem);
  285. }
  286. function preloadImages(srcUrls, rejects = false) {
  287. const promises = srcUrls.map((src) => new Promise((res, rej) => {
  288. const image = new Image();
  289. image.src = src;
  290. image.addEventListener("load", () => res(image));
  291. image.addEventListener("error", (evt) => rejects && rej(evt));
  292. }));
  293. return Promise.allSettled(promises);
  294. }
  295. function openInNewTab(href) {
  296. const openElem = document.createElement("a");
  297. Object.assign(openElem, {
  298. className: "userutils-open-in-new-tab",
  299. target: "_blank",
  300. rel: "noopener noreferrer",
  301. href
  302. });
  303. openElem.style.display = "none";
  304. document.body.appendChild(openElem);
  305. openElem.click();
  306. setTimeout(openElem.remove, 50);
  307. }
  308. function interceptEvent(eventObject, eventName, predicate = () => true) {
  309. if (typeof Error.stackTraceLimit === "number" && Error.stackTraceLimit < 1e3) {
  310. Error.stackTraceLimit = 1e3;
  311. }
  312. (function(original) {
  313. eventObject.__proto__.addEventListener = function(...args) {
  314. var _a, _b;
  315. const origListener = typeof args[1] === "function" ? args[1] : (_b = (_a = args[1]) == null ? void 0 : _a.handleEvent) != null ? _b : () => void 0;
  316. args[1] = function(...a) {
  317. if (args[0] === eventName && predicate(Array.isArray(a) ? a[0] : a))
  318. return;
  319. else
  320. return origListener.apply(this, a);
  321. };
  322. original.apply(this, args);
  323. };
  324. })(eventObject.__proto__.addEventListener);
  325. }
  326. function interceptWindowEvent(eventName, predicate = () => true) {
  327. return interceptEvent(getUnsafeWindow(), eventName, predicate);
  328. }
  329. function amplifyMedia(mediaElement, initialGain = 1) {
  330. const context = new (window.AudioContext || window.webkitAudioContext)();
  331. const props = {
  332. context,
  333. sourceNode: context.createMediaElementSource(mediaElement),
  334. gainNode: context.createGain(),
  335. /** Sets the gain of the amplifying GainNode */
  336. setGain(gain) {
  337. props.gainNode.gain.value = gain;
  338. },
  339. /** Returns the current gain of the amplifying GainNode */
  340. getGain() {
  341. return props.gainNode.gain.value;
  342. },
  343. /** Whether the amplification is currently enabled */
  344. enabled: false,
  345. /** Enable the amplification for the first time or if it was disabled before */
  346. enable() {
  347. if (props.enabled)
  348. return;
  349. props.enabled = true;
  350. props.sourceNode.connect(props.gainNode);
  351. props.gainNode.connect(props.context.destination);
  352. },
  353. /** Disable the amplification */
  354. disable() {
  355. if (!props.enabled)
  356. return;
  357. props.enabled = false;
  358. props.sourceNode.disconnect(props.gainNode);
  359. props.gainNode.disconnect(props.context.destination);
  360. props.sourceNode.connect(props.context.destination);
  361. }
  362. };
  363. props.setGain(initialGain);
  364. return props;
  365. }
  366. function isScrollable(element) {
  367. const { overflowX, overflowY } = getComputedStyle(element);
  368. return {
  369. vertical: (overflowY === "scroll" || overflowY === "auto") && element.scrollHeight > element.clientHeight,
  370. horizontal: (overflowX === "scroll" || overflowX === "auto") && element.scrollWidth > element.clientWidth
  371. };
  372. }
  373.  
  374. // lib/misc.ts
  375. function autoPlural(word, num) {
  376. if (Array.isArray(num) || num instanceof NodeList)
  377. num = num.length;
  378. return `${word}${num === 1 ? "" : "s"}`;
  379. }
  380. function pauseFor(time) {
  381. return new Promise((res) => {
  382. setTimeout(() => res(), time);
  383. });
  384. }
  385. function debounce(func, timeout = 300) {
  386. let timer;
  387. return function(...args) {
  388. clearTimeout(timer);
  389. timer = setTimeout(() => func.apply(this, args), timeout);
  390. };
  391. }
  392. function fetchAdvanced(_0) {
  393. return __async(this, arguments, function* (url, options = {}) {
  394. const { timeout = 1e4 } = options;
  395. const controller = new AbortController();
  396. const id = setTimeout(() => controller.abort(), timeout);
  397. const res = yield fetch(url, __spreadProps(__spreadValues({}, options), {
  398. signal: controller.signal
  399. }));
  400. clearTimeout(id);
  401. return res;
  402. });
  403. }
  404. function insertValues(str, ...values) {
  405. return str.replace(/%\d/gm, (match) => {
  406. var _a, _b;
  407. const argIndex = Number(match.substring(1)) - 1;
  408. return (_b = (_a = values[argIndex]) != null ? _a : match) == null ? void 0 : _b.toString();
  409. });
  410. }
  411.  
  412. // lib/SelectorObserver.ts
  413. var SelectorObserver = class {
  414. constructor(baseElement, options = {}) {
  415. __publicField(this, "enabled", false);
  416. __publicField(this, "baseElement");
  417. __publicField(this, "observer");
  418. __publicField(this, "observerOptions");
  419. __publicField(this, "listenerMap");
  420. this.baseElement = baseElement;
  421. this.listenerMap = /* @__PURE__ */ new Map();
  422. this.observer = new MutationObserver(() => this.checkAllSelectors());
  423. this.observerOptions = __spreadValues({
  424. childList: true,
  425. subtree: true
  426. }, options);
  427. }
  428. checkAllSelectors() {
  429. for (const [selector, listeners] of this.listenerMap.entries())
  430. this.checkSelector(selector, listeners);
  431. }
  432. checkSelector(selector, listeners) {
  433. var _a;
  434. if (!this.enabled)
  435. return;
  436. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  437. if (!baseElement)
  438. return;
  439. const all = listeners.some((listener) => listener.all);
  440. const one = listeners.some((listener) => !listener.all);
  441. const allElements = all ? baseElement.querySelectorAll(selector) : null;
  442. const oneElement = one ? baseElement.querySelector(selector) : null;
  443. for (const options of listeners) {
  444. if (options.all) {
  445. if (allElements && allElements.length > 0) {
  446. options.listener(allElements);
  447. if (!options.continuous)
  448. this.removeListener(selector, options);
  449. }
  450. } else {
  451. if (oneElement) {
  452. options.listener(oneElement);
  453. if (!options.continuous)
  454. this.removeListener(selector, options);
  455. }
  456. }
  457. if (((_a = this.listenerMap.get(selector)) == null ? void 0 : _a.length) === 0)
  458. this.listenerMap.delete(selector);
  459. }
  460. }
  461. debounce(func, time) {
  462. let timeout;
  463. return function(...args) {
  464. clearTimeout(timeout);
  465. timeout = setTimeout(() => func.apply(this, args), time);
  466. };
  467. }
  468. /**
  469. * Starts observing the children of the base element for changes to the given {@linkcode selector} according to the set {@linkcode options}
  470. * @param selector The selector to observe
  471. * @param options Options for the selector observation
  472. * @param options.listener Gets called whenever the selector was found in the DOM
  473. * @param [options.all] Whether to use `querySelectorAll()` instead - default is false
  474. * @param [options.continuous] Whether to call the listener continuously instead of just once - default is false
  475. * @param [options.debounce] Whether to debounce the listener to reduce calls to `querySelector` or `querySelectorAll` - set undefined or <=0 to disable (default)
  476. */
  477. addListener(selector, options) {
  478. options = __spreadValues({ all: false, continuous: false, debounce: 0 }, options);
  479. if (options.debounce && options.debounce > 0 || this.observerOptions.defaultDebounce && this.observerOptions.defaultDebounce > 0) {
  480. options.listener = this.debounce(
  481. options.listener,
  482. options.debounce || this.observerOptions.defaultDebounce
  483. );
  484. }
  485. if (this.listenerMap.has(selector))
  486. this.listenerMap.get(selector).push(options);
  487. else
  488. this.listenerMap.set(selector, [options]);
  489. this.checkSelector(selector, [options]);
  490. }
  491. /** Disables the observation of the child elements */
  492. disable() {
  493. if (!this.enabled)
  494. return;
  495. this.enabled = false;
  496. this.observer.disconnect();
  497. }
  498. /**
  499. * Enables or reenables the observation of the child elements.
  500. * @param immediatelyCheckSelectors Whether to immediately check if all previously registered selectors exist (default is true)
  501. * @returns Returns true when the observation was enabled, false otherwise (e.g. when the base element wasn't found)
  502. */
  503. enable(immediatelyCheckSelectors = true) {
  504. const baseElement = typeof this.baseElement === "string" ? document.querySelector(this.baseElement) : this.baseElement;
  505. if (this.enabled || !baseElement)
  506. return false;
  507. this.enabled = true;
  508. this.observer.observe(baseElement, this.observerOptions);
  509. if (immediatelyCheckSelectors)
  510. this.checkAllSelectors();
  511. return true;
  512. }
  513. /** Returns whether the observation of the child elements is currently enabled */
  514. isEnabled() {
  515. return this.enabled;
  516. }
  517. /** Removes all listeners that have been registered with {@linkcode addListener()} */
  518. clearListeners() {
  519. this.listenerMap.clear();
  520. }
  521. /**
  522. * Removes all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()}
  523. * @returns Returns true when all listeners for the associated selector were found and removed, false otherwise
  524. */
  525. removeAllListeners(selector) {
  526. return this.listenerMap.delete(selector);
  527. }
  528. /**
  529. * Removes a single listener for the given {@linkcode selector} and {@linkcode options} that has been registered with {@linkcode addListener()}
  530. * @returns Returns true when the listener was found and removed, false otherwise
  531. */
  532. removeListener(selector, options) {
  533. const listeners = this.listenerMap.get(selector);
  534. if (!listeners)
  535. return false;
  536. const index = listeners.indexOf(options);
  537. if (index > -1) {
  538. listeners.splice(index, 1);
  539. return true;
  540. }
  541. return false;
  542. }
  543. /** Returns all listeners that have been registered with {@linkcode addListener()} */
  544. getAllListeners() {
  545. return this.listenerMap;
  546. }
  547. /** Returns all listeners for the given {@linkcode selector} that have been registered with {@linkcode addListener()} */
  548. getListeners(selector) {
  549. return this.listenerMap.get(selector);
  550. }
  551. };
  552.  
  553. // lib/translation.ts
  554. var trans = {};
  555. var curLang;
  556. function tr(key, ...args) {
  557. var _a;
  558. if (!curLang)
  559. return key;
  560. const trText = (_a = trans[curLang]) == null ? void 0 : _a[key];
  561. if (!trText)
  562. return key;
  563. if (args.length > 0 && trText.match(/%\d/)) {
  564. return insertValues(trText, ...args);
  565. }
  566. return trText;
  567. }
  568. tr.addLanguage = (language, translations) => {
  569. trans[language] = translations;
  570. };
  571. tr.setLanguage = (language) => {
  572. curLang = language;
  573. };
  574. tr.getLanguage = () => {
  575. return curLang;
  576. };
  577.  
  578. exports.ConfigManager = ConfigManager;
  579. exports.SelectorObserver = SelectorObserver;
  580. exports.addGlobalStyle = addGlobalStyle;
  581. exports.addParent = addParent;
  582. exports.amplifyMedia = amplifyMedia;
  583. exports.autoPlural = autoPlural;
  584. exports.clamp = clamp;
  585. exports.debounce = debounce;
  586. exports.fetchAdvanced = fetchAdvanced;
  587. exports.getUnsafeWindow = getUnsafeWindow;
  588. exports.insertAfter = insertAfter;
  589. exports.insertValues = insertValues;
  590. exports.interceptEvent = interceptEvent;
  591. exports.interceptWindowEvent = interceptWindowEvent;
  592. exports.isScrollable = isScrollable;
  593. exports.mapRange = mapRange;
  594. exports.openInNewTab = openInNewTab;
  595. exports.pauseFor = pauseFor;
  596. exports.preloadImages = preloadImages;
  597. exports.randRange = randRange;
  598. exports.randomId = randomId;
  599. exports.randomItem = randomItem;
  600. exports.randomItemIndex = randomItemIndex;
  601. exports.randomizeArray = randomizeArray;
  602. exports.takeRandomItem = takeRandomItem;
  603. exports.tr = tr;
  604.  
  605. return exports;
  606.  
  607. })({});

QingJ © 2025

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