UserScript 兼容库

确保不同用户脚本管理器之间兼容性的库

当前为 2024-12-05 提交的版本,查看 最新版本

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.cn-greasyfork.org/scripts/519877/1497523/UserScript%20Compatibility%20Library.js

  1. // ==UserScript==
  2. // @name UserScript Compatibility Library
  3. // @name:en UserScript Compatibility Library
  4. // @name:zh-CN UserScript 兼容库
  5. // @name:ru Библиотека совместимости для пользовательских скриптов
  6. // @name:vi Thư viện tương thích cho userscript
  7. // @namespace https://greasyfork.org/vi/users/1195312-renji-yuusei
  8. // @version 1.5.0
  9. // @description A library to ensure compatibility between different userscript managers
  10. // @description:en A library to ensure compatibility between different userscript managers
  11. // @description:zh-CN 确保不同用户脚本管理器之间兼容性的库
  12. // @description:vi Thư viện đảm bảo tương thích giữa các trình quản lý userscript khác nhau
  13. // @description:ru Библиотека для обеспечения совместимости между различными менеджерами пользовательских скриптов
  14. // @author Yuusei
  15. // @license GPL-3.0-only
  16. // @grant unsafeWindow
  17. // @grant GM_info
  18. // @grant GM.info
  19. // @grant GM_getValue
  20. // @grant GM.getValue
  21. // @grant GM_setValue
  22. // @grant GM.setValue
  23. // @grant GM_deleteValue
  24. // @grant GM.deleteValue
  25. // @grant GM_listValues
  26. // @grant GM.listValues
  27. // @grant GM_xmlhttpRequest
  28. // @grant GM.xmlHttpRequest
  29. // @grant GM_download
  30. // @grant GM.download
  31. // @grant GM_notification
  32. // @grant GM.notification
  33. // @grant GM_addStyle
  34. // @grant GM.addStyle
  35. // @grant GM_registerMenuCommand
  36. // @grant GM.registerMenuCommand
  37. // @grant GM_unregisterMenuCommand
  38. // @grant GM.unregisterMenuCommand
  39. // @grant GM_setClipboard
  40. // @grant GM.setClipboard
  41. // @grant GM_getResourceText
  42. // @grant GM.getResourceText
  43. // @grant GM_getResourceURL
  44. // @grant GM.getResourceURL
  45. // @grant GM_openInTab
  46. // @grant GM.openInTab
  47. // @grant GM_addElement
  48. // @grant GM.addElement
  49. // @grant GM_addValueChangeListener
  50. // @grant GM.addValueChangeListener
  51. // @grant GM_removeValueChangeListener
  52. // @grant GM.removeValueChangeListener
  53. // @grant GM_log
  54. // @grant GM.log
  55. // @grant GM_getTab
  56. // @grant GM.getTab
  57. // @grant GM_saveTab
  58. // @grant GM.saveTab
  59. // @grant GM_getTabs
  60. // @grant GM.getTabs
  61. // @grant GM_cookie
  62. // @grant GM.cookie
  63. // @grant GM_webRequest
  64. // @grant GM.webRequest
  65. // @grant GM_fetch
  66. // @grant GM.fetch
  67. // @grant window.close
  68. // @grant window.focus
  69. // @grant window.onurlchange
  70. // @grant GM_addValueChangeListener
  71. // @grant GM_removeValueChangeListener
  72. // @grant GM_getResourceURL
  73. // @grant GM_notification
  74. // @grant GM_xmlhttpRequest
  75. // @grant GM_openInTab
  76. // @grant GM_registerMenuCommand
  77. // @grant GM_unregisterMenuCommand
  78. // @grant GM_setClipboard
  79. // @grant GM_getResourceText
  80. // @grant GM_addStyle
  81. // @grant GM_download
  82. // @grant GM_cookie.get
  83. // @grant GM_cookie.set
  84. // @grant GM_cookie.delete
  85. // @grant GM_webRequest.listen
  86. // @grant GM_webRequest.onBeforeRequest
  87. // @grant GM_addElement.byTag
  88. // @grant GM_addElement.byId
  89. // @grant GM_addElement.byClass
  90. // @grant GM_addElement.byXPath
  91. // @grant GM_addElement.bySelector
  92. // @grant GM_removeElement
  93. // @grant GM_removeElements
  94. // @grant GM_getElement
  95. // @grant GM_getElements
  96. // @grant GM_addScript
  97. // @grant GM_removeScript
  98. // @grant GM_addLink
  99. // @grant GM_removeLink
  100. // @grant GM_addMeta
  101. // @grant GM_removeMeta
  102. // @grant GM_addIframe
  103. // @grant GM_removeIframe
  104. // @grant GM_addImage
  105. // @grant GM_removeImage
  106. // @grant GM_addVideo
  107. // @grant GM_removeVideo
  108. // @grant GM_addAudio
  109. // @grant GM_removeAudio
  110. // @grant GM_addCanvas
  111. // @grant GM_removeCanvas
  112. // @grant GM_addSvg
  113. // @grant GM_removeSvg
  114. // @grant GM_addObject
  115. // @grant GM_removeObject
  116. // @grant GM_addEmbed
  117. // @grant GM_removeEmbed
  118. // @grant GM_addApplet
  119. // @grant GM_removeApplet
  120. // @run-at document-start
  121. // @license GPL-3.0-only
  122. // ==/UserScript==
  123.  
  124. (function () {
  125. 'use strict';
  126.  
  127. const utils = {
  128. isFunction: function (fn) {
  129. return typeof fn === 'function';
  130. },
  131.  
  132. isUndefined: function (value) {
  133. return typeof value === 'undefined';
  134. },
  135.  
  136. isObject: function (value) {
  137. return value !== null && typeof value === 'object';
  138. },
  139.  
  140. sleep: function (ms) {
  141. return new Promise(resolve => setTimeout(resolve, ms));
  142. },
  143.  
  144. retry: async function (fn, attempts = 3, delay = 1000) {
  145. let lastError;
  146. for (let i = 0; i < attempts; i++) {
  147. try {
  148. return await fn();
  149. } catch (error) {
  150. lastError = error;
  151. if (i === attempts - 1) break;
  152. await this.sleep(delay * Math.pow(2, i));
  153. }
  154. }
  155. throw lastError;
  156. },
  157.  
  158. debounce: function (fn, wait) {
  159. let timeout;
  160. return function (...args) {
  161. clearTimeout(timeout);
  162. timeout = setTimeout(() => fn.apply(this, args), wait);
  163. };
  164. },
  165.  
  166. throttle: function (fn, limit) {
  167. let timeout;
  168. let inThrottle;
  169. return function (...args) {
  170. if (!inThrottle) {
  171. fn.apply(this, args);
  172. inThrottle = true;
  173. clearTimeout(timeout);
  174. timeout = setTimeout(() => (inThrottle = false), limit);
  175. }
  176. };
  177. },
  178.  
  179. // Thêm các tiện ích mới
  180. isArray: function (arr) {
  181. return Array.isArray(arr);
  182. },
  183.  
  184. isString: function (str) {
  185. return typeof str === 'string';
  186. },
  187.  
  188. isNumber: function (num) {
  189. return typeof num === 'number' && !isNaN(num);
  190. },
  191.  
  192. isBoolean: function (bool) {
  193. return typeof bool === 'boolean';
  194. },
  195.  
  196. isNull: function (value) {
  197. return value === null;
  198. },
  199.  
  200. isEmpty: function (value) {
  201. if (this.isArray(value)) return value.length === 0;
  202. if (this.isObject(value)) return Object.keys(value).length === 0;
  203. if (this.isString(value)) return value.trim().length === 0;
  204. return false;
  205. },
  206. };
  207.  
  208. const GMCompat = {
  209. info: (function () {
  210. if (!utils.isUndefined(GM_info)) return GM_info;
  211. if (!utils.isUndefined(GM) && GM.info) return GM.info;
  212. return {};
  213. })(),
  214.  
  215. storageCache: new Map(),
  216. cacheTimestamps: new Map(),
  217. cacheExpiry: 3600000, // 1 hour
  218.  
  219. getValue: async function (key, defaultValue) {
  220. try {
  221. if (this.storageCache.has(key)) {
  222. const timestamp = this.cacheTimestamps.get(key);
  223. if (Date.now() - timestamp < this.cacheExpiry) {
  224. return this.storageCache.get(key);
  225. }
  226. }
  227.  
  228. let value;
  229. if (!utils.isUndefined(GM_getValue)) {
  230. value = GM_getValue(key, defaultValue);
  231. } else if (!utils.isUndefined(GM) && GM.getValue) {
  232. value = await GM.getValue(key, defaultValue);
  233. } else {
  234. value = defaultValue;
  235. }
  236.  
  237. this.storageCache.set(key, value);
  238. this.cacheTimestamps.set(key, Date.now());
  239. return value;
  240. } catch (error) {
  241. console.error('getValue error:', error);
  242. return defaultValue;
  243. }
  244. },
  245.  
  246. setValue: async function (key, value) {
  247. try {
  248. this.storageCache.set(key, value);
  249. this.cacheTimestamps.set(key, Date.now());
  250.  
  251. if (!utils.isUndefined(GM_setValue)) {
  252. return GM_setValue(key, value);
  253. }
  254. if (!utils.isUndefined(GM) && GM.setValue) {
  255. return await GM.setValue(key, value);
  256. }
  257. } catch (error) {
  258. this.storageCache.delete(key);
  259. this.cacheTimestamps.delete(key);
  260. throw new Error('Failed to set value: ' + error.message);
  261. }
  262. },
  263.  
  264. deleteValue: async function (key) {
  265. try {
  266. this.storageCache.delete(key);
  267. this.cacheTimestamps.delete(key);
  268.  
  269. if (!utils.isUndefined(GM_deleteValue)) {
  270. return GM_deleteValue(key);
  271. }
  272. if (!utils.isUndefined(GM) && GM.deleteValue) {
  273. return await GM.deleteValue(key);
  274. }
  275. } catch (error) {
  276. throw new Error('Failed to delete value: ' + error.message);
  277. }
  278. },
  279.  
  280. requestQueue: [],
  281. processingRequest: false,
  282. maxRetries: 3,
  283. retryDelay: 1000,
  284.  
  285. xmlHttpRequest: async function (details) {
  286. const makeRequest = () => {
  287. return new Promise((resolve, reject) => {
  288. try {
  289. const callbacks = {
  290. onload: resolve,
  291. onerror: reject,
  292. ontimeout: reject,
  293. onprogress: details.onprogress,
  294. onreadystatechange: details.onreadystatechange,
  295. };
  296.  
  297. const finalDetails = {
  298. timeout: 30000,
  299. ...details,
  300. ...callbacks,
  301. };
  302.  
  303. if (!utils.isUndefined(GM_xmlhttpRequest)) {
  304. GM_xmlhttpRequest(finalDetails);
  305. } else if (!utils.isUndefined(GM) && GM.xmlHttpRequest) {
  306. GM.xmlHttpRequest(finalDetails);
  307. } else if (!utils.isUndefined(GM_fetch)) {
  308. GM_fetch(finalDetails.url, finalDetails);
  309. } else if (!utils.isUndefined(GM) && GM.fetch) {
  310. GM.fetch(finalDetails.url, finalDetails);
  311. } else {
  312. reject(new Error('XMLHttpRequest API not available'));
  313. }
  314. } catch (error) {
  315. reject(error);
  316. }
  317. });
  318. };
  319.  
  320. return utils.retry(makeRequest, this.maxRetries, this.retryDelay);
  321. },
  322.  
  323. download: async function (details) {
  324. try {
  325. const downloadWithProgress = {
  326. ...details,
  327. onprogress: details.onprogress,
  328. onerror: details.onerror,
  329. onload: details.onload,
  330. };
  331.  
  332. if (!utils.isUndefined(GM_download)) {
  333. return new Promise((resolve, reject) => {
  334. GM_download({
  335. ...downloadWithProgress,
  336. onload: resolve,
  337. onerror: reject,
  338. });
  339. });
  340. }
  341. if (!utils.isUndefined(GM) && GM.download) {
  342. return await GM.download(downloadWithProgress);
  343. }
  344. throw new Error('Download API not available');
  345. } catch (error) {
  346. throw new Error('Download failed: ' + error.message);
  347. }
  348. },
  349.  
  350. notification: function (details) {
  351. return new Promise((resolve, reject) => {
  352. try {
  353. const defaultOptions = {
  354. timeout: 5000,
  355. highlight: false,
  356. silent: false,
  357. requireInteraction: false,
  358. priority: 0,
  359. };
  360.  
  361. const callbacks = {
  362. onclick: utils.debounce((...args) => {
  363. if (details.onclick) details.onclick(...args);
  364. resolve('clicked');
  365. }, 300),
  366. ondone: (...args) => {
  367. if (details.ondone) details.ondone(...args);
  368. resolve('closed');
  369. },
  370. onerror: (...args) => {
  371. if (details.onerror) details.onerror(...args);
  372. reject('error');
  373. },
  374. };
  375.  
  376. const finalDetails = { ...defaultOptions, ...details, ...callbacks };
  377.  
  378. if (!utils.isUndefined(GM_notification)) {
  379. GM_notification(finalDetails);
  380. } else if (!utils.isUndefined(GM) && GM.notification) {
  381. GM.notification(finalDetails);
  382. } else {
  383. if ('Notification' in window) {
  384. Notification.requestPermission().then(permission => {
  385. if (permission === 'granted') {
  386. const notification = new Notification(finalDetails.title, {
  387. body: finalDetails.text,
  388. silent: finalDetails.silent,
  389. icon: finalDetails.image,
  390. tag: finalDetails.tag,
  391. requireInteraction: finalDetails.requireInteraction,
  392. badge: finalDetails.badge,
  393. vibrate: finalDetails.vibrate,
  394. });
  395.  
  396. notification.onclick = callbacks.onclick;
  397. notification.onerror = callbacks.onerror;
  398.  
  399. if (finalDetails.timeout > 0) {
  400. setTimeout(() => {
  401. notification.close();
  402. callbacks.ondone();
  403. }, finalDetails.timeout);
  404. }
  405. } else {
  406. reject(new Error('Notification permission denied'));
  407. }
  408. });
  409. } else {
  410. reject(new Error('Notification API not available'));
  411. }
  412. }
  413. } catch (error) {
  414. reject(error);
  415. }
  416. });
  417. },
  418.  
  419. addStyle: function (css) {
  420. try {
  421. const testStyle = document.createElement('style');
  422. testStyle.textContent = css;
  423. if (testStyle.sheet === null) {
  424. throw new Error('Invalid CSS');
  425. }
  426.  
  427. if (!utils.isUndefined(GM_addStyle)) {
  428. return GM_addStyle(css);
  429. }
  430. if (!utils.isUndefined(GM) && GM.addStyle) {
  431. return GM.addStyle(css);
  432. }
  433.  
  434. const style = document.createElement('style');
  435. style.textContent = css;
  436. style.type = 'text/css';
  437. document.head.appendChild(style);
  438. return style;
  439. } catch (error) {
  440. throw new Error('Failed to add style: ' + error.message);
  441. }
  442. },
  443.  
  444. registerMenuCommand: function (name, fn, accessKey) {
  445. try {
  446. if (!utils.isFunction(fn)) {
  447. throw new Error('Command callback must be a function');
  448. }
  449.  
  450. if (!utils.isUndefined(GM_registerMenuCommand)) {
  451. return GM_registerMenuCommand(name, fn, accessKey);
  452. }
  453. if (!utils.isUndefined(GM) && GM.registerMenuCommand) {
  454. return GM.registerMenuCommand(name, fn, accessKey);
  455. }
  456. } catch (error) {
  457. throw new Error('Failed to register menu command: ' + error.message);
  458. }
  459. },
  460.  
  461. setClipboard: function (text, info) {
  462. try {
  463. if (!utils.isUndefined(GM_setClipboard)) {
  464. return GM_setClipboard(text, info);
  465. }
  466. if (!utils.isUndefined(GM) && GM.setClipboard) {
  467. return GM.setClipboard(text, info);
  468. }
  469. return navigator.clipboard.writeText(text);
  470. } catch (error) {
  471. throw new Error('Failed to set clipboard: ' + error.message);
  472. }
  473. },
  474.  
  475. getResourceText: async function (name) {
  476. try {
  477. if (!utils.isUndefined(GM_getResourceText)) {
  478. return GM_getResourceText(name);
  479. }
  480. if (!utils.isUndefined(GM) && GM.getResourceText) {
  481. return await GM.getResourceText(name);
  482. }
  483. throw new Error('Resource API not available');
  484. } catch (error) {
  485. throw new Error('Failed to get resource text: ' + error.message);
  486. }
  487. },
  488.  
  489. getResourceURL: async function (name) {
  490. try {
  491. if (!utils.isUndefined(GM_getResourceURL)) {
  492. return GM_getResourceURL(name);
  493. }
  494. if (!utils.isUndefined(GM) && GM.getResourceURL) {
  495. return await GM.getResourceURL(name);
  496. }
  497. throw new Error('Resource URL API not available');
  498. } catch (error) {
  499. throw new Error('Failed to get resource URL: ' + error.message);
  500. }
  501. },
  502.  
  503. openInTab: function (url, options = {}) {
  504. try {
  505. const defaultOptions = {
  506. active: true,
  507. insert: true,
  508. setParent: true,
  509. };
  510.  
  511. const finalOptions = { ...defaultOptions, ...options };
  512.  
  513. if (!utils.isUndefined(GM_openInTab)) {
  514. return GM_openInTab(url, finalOptions);
  515. }
  516. if (!utils.isUndefined(GM) && GM.openInTab) {
  517. return GM.openInTab(url, finalOptions);
  518. }
  519. return window.open(url, '_blank');
  520. } catch (error) {
  521. throw new Error('Failed to open tab: ' + error.message);
  522. }
  523. },
  524.  
  525. cookie: {
  526. get: async function (details) {
  527. try {
  528. if (!utils.isUndefined(GM_cookie) && GM_cookie.get) {
  529. return await GM_cookie.get(details);
  530. }
  531. if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.get) {
  532. return await GM.cookie.get(details);
  533. }
  534. return document.cookie;
  535. } catch (error) {
  536. throw new Error('Failed to get cookie: ' + error.message);
  537. }
  538. },
  539. set: async function (details) {
  540. try {
  541. if (!utils.isUndefined(GM_cookie) && GM_cookie.set) {
  542. return await GM_cookie.set(details);
  543. }
  544. if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.set) {
  545. return await GM.cookie.set(details);
  546. }
  547. document.cookie = details;
  548. } catch (error) {
  549. throw new Error('Failed to set cookie: ' + error.message);
  550. }
  551. },
  552. delete: async function (details) {
  553. try {
  554. if (!utils.isUndefined(GM_cookie) && GM_cookie.delete) {
  555. return await GM_cookie.delete(details);
  556. }
  557. if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.delete) {
  558. return await GM.cookie.delete(details);
  559. }
  560. } catch (error) {
  561. throw new Error('Failed to delete cookie: ' + error.message);
  562. }
  563. },
  564. },
  565.  
  566. webRequest: {
  567. listen: function (filter, callback) {
  568. try {
  569. if (!utils.isUndefined(GM_webRequest) && GM_webRequest.listen) {
  570. return GM_webRequest.listen(filter, callback);
  571. }
  572. if (!utils.isUndefined(GM) && GM.webRequest && GM.webRequest.listen) {
  573. return GM.webRequest.listen(filter, callback);
  574. }
  575. } catch (error) {
  576. throw new Error('Failed to listen to web request: ' + error.message);
  577. }
  578. },
  579. onBeforeRequest: function (filter, callback) {
  580. try {
  581. if (!utils.isUndefined(GM_webRequest) && GM_webRequest.onBeforeRequest) {
  582. return GM_webRequest.onBeforeRequest(filter, callback);
  583. }
  584. if (!utils.isUndefined(GM) && GM.webRequest && GM.webRequest.onBeforeRequest) {
  585. return GM.webRequest.onBeforeRequest(filter, callback);
  586. }
  587. } catch (error) {
  588. throw new Error('Failed to handle onBeforeRequest: ' + error.message);
  589. }
  590. },
  591. },
  592.  
  593. dom: {
  594. addElement: function (tag, attributes = {}, parent = document.body) {
  595. try {
  596. const element = document.createElement(tag);
  597. Object.entries(attributes).forEach(([key, value]) => {
  598. element.setAttribute(key, value);
  599. });
  600. parent.appendChild(element);
  601. return element;
  602. } catch (error) {
  603. throw new Error('Failed to add element: ' + error.message);
  604. }
  605. },
  606.  
  607. removeElement: function (element) {
  608. try {
  609. if (element && element.parentNode) {
  610. element.parentNode.removeChild(element);
  611. }
  612. } catch (error) {
  613. throw new Error('Failed to remove element: ' + error.message);
  614. }
  615. },
  616.  
  617. getElement: function (selector) {
  618. try {
  619. return document.querySelector(selector);
  620. } catch (error) {
  621. throw new Error('Failed to get element: ' + error.message);
  622. }
  623. },
  624.  
  625. getElements: function (selector) {
  626. try {
  627. return Array.from(document.querySelectorAll(selector));
  628. } catch (error) {
  629. throw new Error('Failed to get elements: ' + error.message);
  630. }
  631. },
  632. },
  633. };
  634.  
  635. const exportGMCompat = function () {
  636. try {
  637. const target = !utils.isUndefined(unsafeWindow) ? unsafeWindow : window;
  638. Object.defineProperty(target, 'GMCompat', {
  639. value: GMCompat,
  640. writable: false,
  641. configurable: false,
  642. enumerable: true,
  643. });
  644.  
  645. if (window.onurlchange !== undefined) {
  646. window.addEventListener('urlchange', () => {});
  647. }
  648. } catch (error) {
  649. console.error('Failed to export GMCompat:', error);
  650. }
  651. };
  652.  
  653. exportGMCompat();
  654. })();