userscripts-core-library

Core library to be used on different userscripts

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.cn-greasyfork.org/scripts/476017/1357292/userscripts-core-library.js

  1. // ==UserScript==
  2. // @name userscripts-core-library
  3. // @version 0.3.0
  4. // @author lucianjp
  5. // @description Core library to handle webpages dom with userscripts from document-start
  6. // ==/UserScript==
  7. // https://greasyfork.org/scripts/476017-userscripts-core-library/code/userscripts-core-library.js
  8.  
  9. //polyfills
  10. if (typeof GM == 'undefined') {
  11. this.GM = {};
  12. }
  13.  
  14. class UserJsCore {
  15. constructor() {
  16. throw new Error('UserJsCore cannot be instantiated.');
  17. }
  18. static ready = (callback) =>
  19. document.readyState !== "loading"
  20. ? callback()
  21. : document.addEventListener("DOMContentLoaded", callback);
  22.  
  23. static addStyle = (aCss) => {
  24. let head = document.getElementsByTagName("head")[0];
  25. if (!head) {
  26. console.error("Head element not found. Cannot add style.");
  27. return null;
  28. }
  29.  
  30. let style = document.createElement("style");
  31. style.setAttribute("type", "text/css");
  32. style.textContent = aCss;
  33. head.appendChild(style);
  34. return style;
  35. };
  36.  
  37. static observe = (observableCollection, continuous = false) => {
  38. const observables = Array.from(observableCollection.entries()).filter(
  39. ([_, observable]) => observable instanceof UserJsCore.ObservableAll || !observable.currentValue
  40. );
  41.  
  42. const observer = new MutationObserver(function (mutations) {
  43. for (var i = mutations.length - 1; i >= 0; i--) {
  44. const mutation = mutations[i];
  45. const addedNodesLength = mutation.addedNodes.length;
  46. if (addedNodesLength > 0) {
  47. for (var j = addedNodesLength - 1; j >= 0; j--) {
  48. const $node = mutation.addedNodes[j];
  49. if ($node && $node.nodeType === 1) {
  50. let observablesLength = observables.length;
  51. for (let k = observablesLength - 1; k >= 0; k--) {
  52. const [_, observable] = observables[k];
  53.  
  54. if (observable.test($node)) {
  55. if(observable instanceof UserJsCore.Observable) {
  56. observable.set($node);
  57. const last = observables.pop();
  58. if (k < observablesLength - 1) observables[k] = last;
  59. observablesLength = observablesLength - 1;
  60. }
  61. if(observable instanceof UserJsCore.ObservableAll){
  62. observable.currentValue.includes($node) || observable.add($node);
  63. }
  64. break;
  65. }
  66. }
  67. }
  68. }
  69.  
  70. if (observables.length === 0 && !continuous) {
  71. observer.disconnect();
  72. return;
  73. }
  74. }
  75. }
  76. });
  77.  
  78. observer.observe(document, { childList: true, subtree: true });
  79.  
  80. if (!continuous) UserJsCore.ready(() => observer.disconnect());
  81.  
  82. return observer;
  83. };
  84.  
  85. static Observable = class {
  86. constructor(lookup, test) {
  87. this.value = undefined;
  88. this.callbacks = [];
  89. this.lookup = lookup;
  90. this.test = test;
  91. if (typeof lookup === "function") {
  92. this.value = lookup();
  93. }
  94. }
  95. set(newValue) {
  96. this.value = newValue;
  97. this.executeCallbacks(this.value);
  98. }
  99. then(callback) {
  100. if (typeof callback === "function") {
  101. this.callbacks.push(callback);
  102. if (this.value) callback(this.value);
  103. }
  104. return this;
  105. }
  106. executeCallbacks(value) {
  107. this.callbacks.forEach((callback) => callback(value));
  108. }
  109. get currentValue() {
  110. return this.value;
  111. }
  112. };
  113.  
  114. static ObservableAll = class {
  115. constructor(lookup, test) {
  116. this.values = [];
  117. this.callbacks = [];
  118. this.lookup = lookup;
  119. this.test = test;
  120. if (typeof lookup === "function") {
  121. this.values = [...lookup()];
  122. }
  123. }
  124. add(newValue) {
  125. this.values.push(newValue);
  126. this.executeCallbacks(newValue);
  127. }
  128. then(callback) {
  129. if (typeof callback === "function") {
  130. this.callbacks.push(callback);
  131. if (this.values.length > 0)
  132. this.values.forEach((value) => callback(value));
  133. }
  134. return this;
  135. }
  136. executeCallbacks(value) {
  137. this.callbacks.forEach((callback) => callback(value));
  138. }
  139. get currentValue() {
  140. return this.values;
  141. }
  142. }
  143. static ObservableCollection = class extends Map {
  144. constructor() {
  145. super();
  146. }
  147. add(name, observable) {
  148. this.set(name, observable);
  149. return observable;
  150. }
  151. }
  152. static Config = class {
  153. static #config;
  154. static #isInitializedPromise;
  155. constructor() {
  156. throw new Error('Config cannot be instantiated.');
  157. }
  158. static async init(defaultConfig = {}) {
  159. if (!this.#isInitializedPromise) {
  160. this.#isInitializedPromise = (async () => {
  161. if (!this.#config) {
  162. const storedConfig = await GM.getValue('config', {});
  163. this.#config = { ...defaultConfig, ...storedConfig };
  164. }
  165. })();
  166. }
  167. await this.#isInitializedPromise;
  168. return this; // Return the class instance after initialization
  169. }
  170. static get(key) {
  171. if (!this.#isInitializedPromise) {
  172. throw new Error('Config has not been initialized. Call init() first.');
  173. }
  174. return this.#config[key];
  175. }
  176. static set(key, value) {
  177. if (!this.#isInitializedPromise) {
  178. throw new Error('Config has not been initialized. Call init() first.');
  179. }
  180. this.#config[key] = value;
  181. GM.setValue('config', this.#config);
  182. }
  183. }
  184.  
  185. static Feature = class {
  186. constructor(id, name, action) {
  187. this._id = id;
  188. this._name = name;
  189. if (this.enabled == null) {
  190. this.enabled = true;
  191. }
  192. if (this._enabled) {
  193. try{
  194. action();
  195. console.groupCollapsed(name)
  196. console.log(`${name} started`)
  197. } catch (error){
  198. console.group(name)
  199. console.error(error);
  200. }
  201. console.groupEnd();
  202. }
  203. }
  204. set id(id) {
  205. this._id = id;
  206. }
  207. get id() {
  208. return this._id;
  209. }
  210. set name(name) {
  211. this._name = name;
  212. }
  213. get name() {
  214. return this._name;
  215. }
  216. set enabled(enabled) {
  217. this._enabled = enabled;
  218. UserJsCore.Config.set(`feature_${this._id}`, this._enabled);
  219. }
  220. get enabled() {
  221. return this._enabled || (this._enabled = UserJsCore.Config.get(`feature_${this._id}`));
  222. }
  223. get displayName() {
  224. return `${this._enabled ? "Disable" : "Enable"} ${this._name}`;
  225. }
  226. toggle() {
  227. this.enabled = !this.enabled;
  228. }
  229. }
  230.  
  231. static Menu = class {
  232. static #menuIds = [];
  233. static #features;
  234. static #notification;
  235. static initialize(features, notificationChange) {
  236. if(GM.registerMenuCommand === undefined){
  237. throw new Error("UserJsCore.Menu needs the GM.registerMenuCommand granted");
  238. }
  239. if(GM.unregisterMenuCommand === undefined){
  240. throw new Error("UserJsCore.Menu needs the GM.unregisterMenuCommand granted");
  241. }
  242. this.#features = Object.values(features);
  243. this.#notification = notificationChange;
  244. this.#generateMenu();
  245. }
  246. static #generateMenu() {
  247. if (this.#menuIds.length > 0 && this.#notification) {
  248. this.#notification();
  249. }
  250. this.#menuIds.forEach((id) => GM.unregisterMenuCommand(id));
  251. for (const feature of this.#features) {
  252. this.#menuIds.push(
  253. GM.registerMenuCommand(feature.displayName, () => {
  254. feature.toggle();
  255. this.#generateMenu();
  256. })
  257. );
  258. }
  259. }
  260. }
  261.  
  262. static AsyncQueue = class {
  263. constructor(concurrentLimit = 6) {
  264. this.concurrentLimit = concurrentLimit;
  265. this.runningCount = 0;
  266. this.queue = [];
  267. this.isPaused = false;
  268. }
  269. async enqueueAsync(func, priority = 0) {
  270. return new Promise((resolve, reject) => {
  271. const taskId = Symbol(); // Generate a unique ID for each task
  272. const task = {
  273. id: taskId,
  274. func,
  275. priority,
  276. resolve,
  277. reject,
  278. };
  279. const execute = async (task) => {
  280. if (this.isPaused) {
  281. this.queue.unshift(task);
  282. this.logQueueStatus();
  283. return;
  284. }
  285. this.runningCount++;
  286. this.logQueueStatus();
  287. try {
  288. const result = await task.func();
  289. task.resolve(result);
  290. } catch (error) {
  291. task.reject(error);
  292. } finally {
  293. this.runningCount--;
  294. if (this.queue.length > 0) {
  295. //this.queue.sort((a, b) => b.priority - a.priority);
  296. const nextTask = this.queue.shift();
  297. execute(nextTask);
  298. }
  299. this.logQueueStatus();
  300. }
  301. };
  302. this.logQueueStatus();
  303. if (this.runningCount < this.concurrentLimit) {
  304. execute(task);
  305. } else {
  306. this.queue.push(task);
  307. //this.queue.sort((a, b) => b.priority - a.priority);
  308. }
  309. });
  310. }
  311. cancelTask(taskId) {
  312. const index = this.queue.findIndex((task) => task.id === taskId);
  313. if (index !== -1) {
  314. const [canceledTask] = this.queue.splice(index, 1);
  315. canceledTask.reject(new Error('Task canceled'));
  316. }
  317. }
  318.  
  319. logQueueStatus() {
  320. //console.log(`Running: ${this.runningCount}, Queued: ${this.queue.length}`);
  321. }
  322. clearQueue() {
  323. this.queue.forEach((task) => task.reject(new Error('Queue cleared')));
  324. this.queue = [];
  325. }
  326. pause() {
  327. this.isPaused = true;
  328. this.logQueueStatus();
  329. }
  330. resume() {
  331. this.isPaused = false;
  332. if (this.queue.length > 0) {
  333. this.queue.sort((a, b) => b.priority - a.priority);
  334. const nextTask = this.queue.shift();
  335. this.enqueueAsync(nextTask.func, nextTask.priority);
  336. }
  337. this.logQueueStatus();
  338. }
  339. }
  340.  
  341. static Cache = class {
  342. constructor(props = {}) {
  343. this.version = props.version ?? 1;
  344. this.name = props.dbName ?? window.location.origin;
  345. this.storeName = props.storeName ?? 'cache';
  346. this.db = null;
  347. this.concurrentRequests = props.concurrentRequests ?? 6;
  348.  
  349. this.queue = new UserJsCore.AsyncQueue(this.concurrentRequests);
  350. }
  351.  
  352. init() {
  353. if(GM.xmlHttpRequest === undefined){
  354. throw new Error("UserJsCore.Cache needs the GM.xmlHttpRequest granted");
  355. }
  356.  
  357. return new Promise(resolve => {
  358. if(this.db) resolve(this);
  359.  
  360. const request = indexedDB.open(this.name, this.version);
  361. request.onupgradeneeded = event => {
  362. event.target.result.createObjectStore(this.storeName);
  363. };
  364. request.onsuccess = () => {
  365. this.db = request.result;
  366. this.db.onerror = () => {
  367. console.error('Error creating/accessing db');
  368. };
  369. if (this.db.setVersion && this.db.version !== this.version) {
  370. const version = this.db.setVersion(this.version);
  371. version.onsuccess = () => {
  372. this.db.createObjectStore(this.storeName);
  373. resolve(this);
  374. };
  375. } else {
  376. resolve(this);
  377. }
  378. };
  379. });
  380. }
  381. putImage(key, url) {
  382. return this.queue.enqueueAsync(async () => {
  383. if (!this.db) {
  384. throw new Error('DB not initialized. Call the init method');
  385. }
  386.  
  387. try {
  388. const blob = await new Promise((resolve, reject) => {
  389. console.log(`requesting : ${url}`)
  390. GM.xmlHttpRequest({
  391. method: 'GET',
  392. url: url,
  393. responseType: 'blob',
  394. onload: (event) => resolve(event.response),
  395. onerror: (e) => reject(e),
  396. });
  397. });
  398. // Check if the blob is a valid image
  399. if (!(blob instanceof Blob) || blob.type.indexOf('image') === -1) {
  400. throw new Error('The response does not contain a valid image.');
  401. }
  402. const transaction = this.db.transaction(this.storeName, 'readwrite');
  403. transaction.objectStore(this.storeName).put(blob, key);
  404. return URL.createObjectURL(blob);
  405. } catch (error) {
  406. console.error(error);
  407. throw error;
  408. }
  409. });
  410. }
  411. getImage(key) {
  412. return new Promise((resolve, reject) => {
  413. if (!this.db) {
  414. return reject('DB not initialized. Call the init method');
  415. }
  416.  
  417. const transaction = this.db.transaction(this.storeName, 'readonly');
  418. const request = transaction.objectStore(this.storeName).get(key);
  419. request.onsuccess = event => {
  420. const result = event?.target?.result;
  421. if(result)
  422. resolve(URL.createObjectURL(result));
  423. else
  424. resolve();
  425. };
  426.  
  427. request.onerror = (event) => {
  428. const error = event?.target?.error;
  429. reject(error);
  430. };
  431. });
  432. }
  433.  
  434. clear() {
  435. return new Promise(resolve => {
  436. if (!this.db)
  437. return reject('DB not initialized. Call the init method');
  438.  
  439. const transaction = this.db.transaction(this.storeName, "readwrite");
  440. const request = transaction.objectStore(this.storeName).clear();
  441. request.onsuccess = () => {
  442. resolve();
  443. };
  444. });
  445. }
  446. }
  447. };