UserScript Compatibility Library

A library to ensure compatibility between different userscript managers

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/519877/1507987/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 2024.12.23.1
  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. // @grant GM_addValueChangeListener.remove
  123. // @grant GM_getResourceURL.blob
  124. // @grant GM_notification.close
  125. // @grant GM_openInTab.focus
  126. // @grant GM_setClipboard.format
  127. // @grant GM_xmlhttpRequest.abort
  128. // @grant GM_download.progress
  129. // @grant GM_cookie.list
  130. // @grant GM_cookie.deleteAll
  131. // @grant GM_webRequest.filter
  132. // @grant GM_addElement.create
  133. // @grant GM_removeElement.all
  134. // @grant GM_getElement.all
  135. // @grant GM_addScript.remote
  136. // @grant GM_addLink.stylesheet
  137. // @grant GM_addMeta.viewport
  138. // @grant GM_addIframe.sandbox
  139. // @grant GM_addImage.lazy
  140. // @grant GM_addVideo.controls
  141. // @grant GM_addAudio.autoplay
  142. // @grant GM_addCanvas.context
  143. // @grant GM_addSvg.namespace
  144. // @grant GM_addObject.data
  145. // @grant GM_addEmbed.type
  146. // @grant GM_addApplet.code
  147. // ==/UserScript==
  148. (function () {
  149. 'use strict';
  150. const utils = {
  151. isFunction: function (fn) {
  152. return typeof fn === 'function';
  153. },
  154. isUndefined: function (value) {
  155. return typeof value === 'undefined';
  156. },
  157. isObject: function (value) {
  158. return value !== null && typeof value === 'object';
  159. },
  160. sleep: function (ms) {
  161. return new Promise(resolve => setTimeout(resolve, ms));
  162. },
  163. retry: async function (fn, attempts = 3, delay = 1000) {
  164. let lastError;
  165. for (let i = 0; i < attempts; i++) {
  166. try {
  167. return await fn();
  168. } catch (error) {
  169. lastError = error;
  170. if (i === attempts - 1) break;
  171. await this.sleep(delay * Math.pow(2, i));
  172. }
  173. }
  174. throw lastError;
  175. },
  176. debounce: function (fn, wait) {
  177. let timeout;
  178. return function (...args) {
  179. clearTimeout(timeout);
  180. timeout = setTimeout(() => fn.apply(this, args), wait);
  181. };
  182. },
  183. throttle: function (fn, limit) {
  184. let timeout;
  185. let inThrottle;
  186. return function (...args) {
  187. if (!inThrottle) {
  188. fn.apply(this, args);
  189. inThrottle = true;
  190. clearTimeout(timeout);
  191. timeout = setTimeout(() => (inThrottle = false), limit);
  192. }
  193. };
  194. },
  195. // Thêm các tiện ích mới
  196. isArray: function (arr) {
  197. return Array.isArray(arr);
  198. },
  199. isString: function (str) {
  200. return typeof str === 'string';
  201. },
  202. isNumber: function (num) {
  203. return typeof num === 'number' && !isNaN(num);
  204. },
  205. isBoolean: function (bool) {
  206. return typeof bool === 'boolean';
  207. },
  208. isNull: function (value) {
  209. return value === null;
  210. },
  211. isEmpty: function (value) {
  212. if (this.isArray(value)) return value.length === 0;
  213. if (this.isObject(value)) return Object.keys(value).length === 0;
  214. if (this.isString(value)) return value.trim().length === 0;
  215. return false;
  216. },
  217. };
  218. const GMCompat = {
  219. info: (function () {
  220. if (!utils.isUndefined(GM_info)) return GM_info;
  221. if (!utils.isUndefined(GM) && GM.info) return GM.info;
  222. return {};
  223. })(),
  224. storageCache: new Map(),
  225. cacheTimestamps: new Map(),
  226. cacheExpiry: 3600000, // 1 hour
  227. getValue: async function (key, defaultValue) {
  228. try {
  229. if (this.storageCache.has(key)) {
  230. const timestamp = this.cacheTimestamps.get(key);
  231. if (Date.now() - timestamp < this.cacheExpiry) {
  232. return this.storageCache.get(key);
  233. }
  234. }
  235. let value;
  236. if (!utils.isUndefined(GM_getValue)) {
  237. value = GM_getValue(key, defaultValue);
  238. } else if (!utils.isUndefined(GM) && GM.getValue) {
  239. value = await GM.getValue(key, defaultValue);
  240. } else {
  241. value = defaultValue;
  242. }
  243. this.storageCache.set(key, value);
  244. this.cacheTimestamps.set(key, Date.now());
  245. return value;
  246. } catch (error) {
  247. console.error('getValue error:', error);
  248. return defaultValue;
  249. }
  250. },
  251. setValue: async function (key, value) {
  252. try {
  253. this.storageCache.set(key, value);
  254. this.cacheTimestamps.set(key, Date.now());
  255. if (!utils.isUndefined(GM_setValue)) {
  256. return GM_setValue(key, value);
  257. }
  258. if (!utils.isUndefined(GM) && GM.setValue) {
  259. return await GM.setValue(key, value);
  260. }
  261. } catch (error) {
  262. this.storageCache.delete(key);
  263. this.cacheTimestamps.delete(key);
  264. throw new Error('Failed to set value: ' + error.message);
  265. }
  266. },
  267. deleteValue: async function (key) {
  268. try {
  269. this.storageCache.delete(key);
  270. this.cacheTimestamps.delete(key);
  271. if (!utils.isUndefined(GM_deleteValue)) {
  272. return GM_deleteValue(key);
  273. }
  274. if (!utils.isUndefined(GM) && GM.deleteValue) {
  275. return await GM.deleteValue(key);
  276. }
  277. } catch (error) {
  278. throw new Error('Failed to delete value: ' + error.message);
  279. }
  280. },
  281. requestQueue: [],
  282. processingRequest: false,
  283. maxRetries: 3,
  284. retryDelay: 1000,
  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. const finalDetails = {
  297. timeout: 30000,
  298. ...details,
  299. ...callbacks,
  300. };
  301. if (!utils.isUndefined(GM_xmlhttpRequest)) {
  302. GM_xmlhttpRequest(finalDetails);
  303. } else if (!utils.isUndefined(GM) && GM.xmlHttpRequest) {
  304. GM.xmlHttpRequest(finalDetails);
  305. } else if (!utils.isUndefined(GM_fetch)) {
  306. GM_fetch(finalDetails.url, finalDetails);
  307. } else if (!utils.isUndefined(GM) && GM.fetch) {
  308. GM.fetch(finalDetails.url, finalDetails);
  309. } else {
  310. reject(new Error('XMLHttpRequest API not available'));
  311. }
  312. } catch (error) {
  313. reject(error);
  314. }
  315. });
  316. };
  317. return utils.retry(makeRequest, this.maxRetries, this.retryDelay);
  318. },
  319. download: async function (details) {
  320. try {
  321. const downloadWithProgress = {
  322. ...details,
  323. onprogress: details.onprogress,
  324. onerror: details.onerror,
  325. onload: details.onload,
  326. };
  327. if (!utils.isUndefined(GM_download)) {
  328. return new Promise((resolve, reject) => {
  329. GM_download({
  330. ...downloadWithProgress,
  331. onload: resolve,
  332. onerror: reject,
  333. });
  334. });
  335. }
  336. if (!utils.isUndefined(GM) && GM.download) {
  337. return await GM.download(downloadWithProgress);
  338. }
  339. throw new Error('Download API not available');
  340. } catch (error) {
  341. throw new Error('Download failed: ' + error.message);
  342. }
  343. },
  344. notification: function (details) {
  345. return new Promise((resolve, reject) => {
  346. try {
  347. const defaultOptions = {
  348. timeout: 5000,
  349. highlight: false,
  350. silent: false,
  351. requireInteraction: false,
  352. priority: 0,
  353. };
  354. const callbacks = {
  355. onclick: utils.debounce((...args) => {
  356. if (details.onclick) details.onclick(...args);
  357. resolve('clicked');
  358. }, 300),
  359. ondone: (...args) => {
  360. if (details.ondone) details.ondone(...args);
  361. resolve('closed');
  362. },
  363. onerror: (...args) => {
  364. if (details.onerror) details.onerror(...args);
  365. reject('error');
  366. },
  367. };
  368. const finalDetails = { ...defaultOptions, ...details, ...callbacks };
  369. if (!utils.isUndefined(GM_notification)) {
  370. GM_notification(finalDetails);
  371. } else if (!utils.isUndefined(GM) && GM.notification) {
  372. GM.notification(finalDetails);
  373. } else {
  374. if ('Notification' in window) {
  375. Notification.requestPermission().then(permission => {
  376. if (permission === 'granted') {
  377. const notification = new Notification(finalDetails.title, {
  378. body: finalDetails.text,
  379. silent: finalDetails.silent,
  380. icon: finalDetails.image,
  381. tag: finalDetails.tag,
  382. requireInteraction: finalDetails.requireInteraction,
  383. badge: finalDetails.badge,
  384. vibrate: finalDetails.vibrate,
  385. });
  386. notification.onclick = callbacks.onclick;
  387. notification.onerror = callbacks.onerror;
  388. if (finalDetails.timeout > 0) {
  389. setTimeout(() => {
  390. notification.close();
  391. callbacks.ondone();
  392. }, finalDetails.timeout);
  393. }
  394. } else {
  395. reject(new Error('Notification permission denied'));
  396. }
  397. });
  398. } else {
  399. reject(new Error('Notification API not available'));
  400. }
  401. }
  402. } catch (error) {
  403. reject(error);
  404. }
  405. });
  406. },
  407. addStyle: function (css) {
  408. try {
  409. const testStyle = document.createElement('style');
  410. testStyle.textContent = css;
  411. if (testStyle.sheet === null) {
  412. throw new Error('Invalid CSS');
  413. }
  414. if (!utils.isUndefined(GM_addStyle)) {
  415. return GM_addStyle(css);
  416. }
  417. if (!utils.isUndefined(GM) && GM.addStyle) {
  418. return GM.addStyle(css);
  419. }
  420. const style = document.createElement('style');
  421. style.textContent = css;
  422. style.type = 'text/css';
  423. document.head.appendChild(style);
  424. return style;
  425. } catch (error) {
  426. throw new Error('Failed to add style: ' + error.message);
  427. }
  428. },
  429. registerMenuCommand: function (name, fn, accessKey) {
  430. try {
  431. if (!utils.isFunction(fn)) {
  432. throw new Error('Command callback must be a function');
  433. }
  434. if (!utils.isUndefined(GM_registerMenuCommand)) {
  435. return GM_registerMenuCommand(name, fn, accessKey);
  436. }
  437. if (!utils.isUndefined(GM) && GM.registerMenuCommand) {
  438. return GM.registerMenuCommand(name, fn, accessKey);
  439. }
  440. } catch (error) {
  441. throw new Error('Failed to register menu command: ' + error.message);
  442. }
  443. },
  444. setClipboard: function (text, info) {
  445. try {
  446. if (!utils.isUndefined(GM_setClipboard)) {
  447. return GM_setClipboard(text, info);
  448. }
  449. if (!utils.isUndefined(GM) && GM.setClipboard) {
  450. return GM.setClipboard(text, info);
  451. }
  452. return navigator.clipboard.writeText(text);
  453. } catch (error) {
  454. throw new Error('Failed to set clipboard: ' + error.message);
  455. }
  456. },
  457. getResourceText: async function (name) {
  458. try {
  459. if (!utils.isUndefined(GM_getResourceText)) {
  460. return GM_getResourceText(name);
  461. }
  462. if (!utils.isUndefined(GM) && GM.getResourceText) {
  463. return await GM.getResourceText(name);
  464. }
  465. throw new Error('Resource API not available');
  466. } catch (error) {
  467. throw new Error('Failed to get resource text: ' + error.message);
  468. }
  469. },
  470. getResourceURL: async function (name) {
  471. try {
  472. if (!utils.isUndefined(GM_getResourceURL)) {
  473. return GM_getResourceURL(name);
  474. }
  475. if (!utils.isUndefined(GM) && GM.getResourceURL) {
  476. return await GM.getResourceURL(name);
  477. }
  478. throw new Error('Resource URL API not available');
  479. } catch (error) {
  480. throw new Error('Failed to get resource URL: ' + error.message);
  481. }
  482. },
  483. openInTab: function (url, options = {}) {
  484. try {
  485. const defaultOptions = {
  486. active: true,
  487. insert: true,
  488. setParent: true,
  489. };
  490. const finalOptions = { ...defaultOptions, ...options };
  491. if (!utils.isUndefined(GM_openInTab)) {
  492. return GM_openInTab(url, finalOptions);
  493. }
  494. if (!utils.isUndefined(GM) && GM.openInTab) {
  495. return GM.openInTab(url, finalOptions);
  496. }
  497. return window.open(url, '_blank');
  498. } catch (error) {
  499. throw new Error('Failed to open tab: ' + error.message);
  500. }
  501. },
  502. cookie: {
  503. get: async function (details) {
  504. try {
  505. if (!utils.isUndefined(GM_cookie) && GM_cookie.get) {
  506. return await GM_cookie.get(details);
  507. }
  508. if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.get) {
  509. return await GM.cookie.get(details);
  510. }
  511. return document.cookie;
  512. } catch (error) {
  513. throw new Error('Failed to get cookie: ' + error.message);
  514. }
  515. },
  516. set: async function (details) {
  517. try {
  518. if (!utils.isUndefined(GM_cookie) && GM_cookie.set) {
  519. return await GM_cookie.set(details);
  520. }
  521. if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.set) {
  522. return await GM.cookie.set(details);
  523. }
  524. document.cookie = details;
  525. } catch (error) {
  526. throw new Error('Failed to set cookie: ' + error.message);
  527. }
  528. },
  529. delete: async function (details) {
  530. try {
  531. if (!utils.isUndefined(GM_cookie) && GM_cookie.delete) {
  532. return await GM_cookie.delete(details);
  533. }
  534. if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.delete) {
  535. return await GM.cookie.delete(details);
  536. }
  537. } catch (error) {
  538. throw new Error('Failed to delete cookie: ' + error.message);
  539. }
  540. },
  541. },
  542. webRequest: {
  543. listen: function (filter, callback) {
  544. try {
  545. if (!utils.isUndefined(GM_webRequest) && GM_webRequest.listen) {
  546. return GM_webRequest.listen(filter, callback);
  547. }
  548. if (!utils.isUndefined(GM) && GM.webRequest && GM.webRequest.listen) {
  549. return GM.webRequest.listen(filter, callback);
  550. }
  551. } catch (error) {
  552. throw new Error('Failed to listen to web request: ' + error.message);
  553. }
  554. },
  555. onBeforeRequest: function (filter, callback) {
  556. try {
  557. if (!utils.isUndefined(GM_webRequest) && GM_webRequest.onBeforeRequest) {
  558. return GM_webRequest.onBeforeRequest(filter, callback);
  559. }
  560. if (!utils.isUndefined(GM) && GM.webRequest && GM.webRequest.onBeforeRequest) {
  561. return GM.webRequest.onBeforeRequest(filter, callback);
  562. }
  563. } catch (error) {
  564. throw new Error('Failed to handle onBeforeRequest: ' + error.message);
  565. }
  566. },
  567. },
  568. dom: {
  569. addElement: function (tag, attributes = {}, parent = document.body) {
  570. try {
  571. const element = document.createElement(tag);
  572. Object.entries(attributes).forEach(([key, value]) => {
  573. element.setAttribute(key, value);
  574. });
  575. parent.appendChild(element);
  576. return element;
  577. } catch (error) {
  578. throw new Error('Failed to add element: ' + error.message);
  579. }
  580. },
  581. removeElement: function (element) {
  582. try {
  583. if (element && element.parentNode) {
  584. element.parentNode.removeChild(element);
  585. }
  586. } catch (error) {
  587. throw new Error('Failed to remove element: ' + error.message);
  588. }
  589. },
  590. getElement: function (selector) {
  591. try {
  592. return document.querySelector(selector);
  593. } catch (error) {
  594. throw new Error('Failed to get element: ' + error.message);
  595. }
  596. },
  597. getElements: function (selector) {
  598. try {
  599. return Array.from(document.querySelectorAll(selector));
  600. } catch (error) {
  601. throw new Error('Failed to get elements: ' + error.message);
  602. }
  603. },
  604. },
  605. storage: {
  606. setObject: async function (key, obj) {
  607. try {
  608. const jsonStr = JSON.stringify(obj);
  609. return await GMCompat.setValue(key, jsonStr);
  610. } catch (error) {
  611. utils.logger.error('setObject failed:', error);
  612. throw error;
  613. }
  614. },
  615. getObject: async function (key, defaultValue = null) {
  616. try {
  617. const jsonStr = await GMCompat.getValue(key, null);
  618. return jsonStr ? JSON.parse(jsonStr) : defaultValue;
  619. } catch (error) {
  620. utils.logger.error('getObject failed:', error);
  621. return defaultValue;
  622. }
  623. },
  624. appendToArray: async function (key, value) {
  625. try {
  626. const arr = await this.getObject(key, []);
  627. arr.push(value);
  628. await this.setObject(key, arr);
  629. return arr;
  630. } catch (error) {
  631. utils.logger.error('appendToArray failed:', error);
  632. throw error;
  633. }
  634. },
  635. removeFromArray: async function (key, predicate) {
  636. try {
  637. const arr = await this.getObject(key, []);
  638. const filtered = arr.filter(item => !predicate(item));
  639. await this.setObject(key, filtered);
  640. return filtered;
  641. } catch (error) {
  642. utils.logger.error('removeFromArray failed:', error);
  643. throw error;
  644. }
  645. },
  646. },
  647. request: {
  648. downloadWithProgress: async function (url, filename, onProgress) {
  649. try {
  650. return await GMCompat.download({
  651. url: url,
  652. name: filename,
  653. onprogress: onProgress,
  654. saveAs: true,
  655. });
  656. } catch (error) {
  657. utils.logger.error('downloadWithProgress failed:', error);
  658. throw error;
  659. }
  660. },
  661. fetchWithRetry: async function (url, options = {}) {
  662. const finalOptions = {
  663. method: 'GET',
  664. timeout: 10000,
  665. retry: 3,
  666. retryDelay: 1000,
  667. ...options,
  668. };
  669.  
  670. return await utils.retry(
  671. async () => {
  672. return await GMCompat.xmlHttpRequest({
  673. url: url,
  674. ...finalOptions,
  675. });
  676. },
  677. finalOptions.retry,
  678. finalOptions.retryDelay
  679. );
  680. },
  681. },
  682. ui: {
  683. createMenuCommand: function (name, callback, options = {}) {
  684. const defaultOptions = {
  685. accessKey: null,
  686. autoClose: true,
  687. };
  688. const finalOptions = { ...defaultOptions, ...options };
  689.  
  690. return GMCompat.registerMenuCommand(
  691. name,
  692. async (...args) => {
  693. try {
  694. await callback(...args);
  695. if (finalOptions.autoClose) {
  696. window.close();
  697. }
  698. } catch (error) {
  699. utils.logger.error('Menu command failed:', error);
  700. GMCompat.notification({
  701. title: 'Lỗi',
  702. text: `Li thc thi lnh: ${error.message}`,
  703. type: 'error',
  704. });
  705. }
  706. },
  707. finalOptions.accessKey
  708. );
  709. },
  710. toast: function (message, type = 'info', duration = 3000) {
  711. return GMCompat.notification({
  712. title: type.charAt(0).toUpperCase() + type.slice(1),
  713. text: message,
  714. timeout: duration,
  715. onclick: () => {},
  716. silent: true,
  717. highlight: false,
  718. });
  719. },
  720. },
  721. clipboard: {
  722. copyFormatted: async function (text, format = 'text/plain') {
  723. try {
  724. await GMCompat.setClipboard(text, format);
  725. return true;
  726. } catch (error) {
  727. utils.logger.error('copyFormatted failed:', error);
  728. return false;
  729. }
  730. },
  731. copyHTML: async function (html) {
  732. return await this.copyFormatted(html, 'text/html');
  733. },
  734. },
  735. cookies: {
  736. getAll: async function (domain) {
  737. try {
  738. return await GMCompat.cookie.get({ domain: domain });
  739. } catch (error) {
  740. utils.logger.error('getAll cookies failed:', error);
  741. return [];
  742. }
  743. },
  744. setCookie: async function (name, value, options = {}) {
  745. try {
  746. const defaultOptions = {
  747. path: '/',
  748. secure: true,
  749. sameSite: 'Lax',
  750. expirationDate: Math.floor(Date.now() / 1000) + 86400, // 1 day
  751. };
  752.  
  753. await GMCompat.cookie.set({
  754. name: name,
  755. value: value,
  756. ...defaultOptions,
  757. ...options,
  758. });
  759. } catch (error) {
  760. utils.logger.error('setCookie failed:', error);
  761. throw error;
  762. }
  763. },
  764. },
  765. valueChangeListener: {
  766. listeners: new Map(),
  767.  
  768. add: function (name, callback) {
  769. try {
  770. if (typeof GM_addValueChangeListener !== 'undefined') {
  771. const listenerId = GM_addValueChangeListener(name, callback);
  772. this.listeners.set(name, listenerId);
  773. return listenerId;
  774. }
  775. return null;
  776. } catch (error) {
  777. utils.logger.error('Failed to add value change listener:', error);
  778. return null;
  779. }
  780. },
  781.  
  782. remove: function (name) {
  783. try {
  784. const listenerId = this.listeners.get(name);
  785. if (listenerId && typeof GM_removeValueChangeListener !== 'undefined') {
  786. GM_removeValueChangeListener(listenerId);
  787. this.listeners.delete(name);
  788. return true;
  789. }
  790. return false;
  791. } catch (error) {
  792. utils.logger.error('Failed to remove value change listener:', error);
  793. return false;
  794. }
  795. },
  796. },
  797. resource: {
  798. getBlob: async function (name) {
  799. try {
  800. const url = await GMCompat.getResourceURL(name);
  801. const response = await fetch(url);
  802. return await response.blob();
  803. } catch (error) {
  804. utils.logger.error('Failed to get resource blob:', error);
  805. throw error;
  806. }
  807. },
  808.  
  809. getText: async function (name, defaultValue = '') {
  810. try {
  811. return (await GMCompat.getResourceText(name)) || defaultValue;
  812. } catch (error) {
  813. utils.logger.error('Failed to get resource text:', error);
  814. return defaultValue;
  815. }
  816. },
  817. },
  818. tab: {
  819. open: function (url, options = {}) {
  820. const defaultOptions = {
  821. active: true,
  822. insert: true,
  823. setParent: true,
  824. incognito: false,
  825. };
  826.  
  827. try {
  828. return GMCompat.openInTab(url, { ...defaultOptions, ...options });
  829. } catch (error) {
  830. utils.logger.error('Failed to open tab:', error);
  831. return null;
  832. }
  833. },
  834.  
  835. focus: function (tab) {
  836. try {
  837. if (tab && tab.focus) {
  838. tab.focus();
  839. return true;
  840. }
  841. return false;
  842. } catch (error) {
  843. utils.logger.error('Failed to focus tab:', error);
  844. return false;
  845. }
  846. },
  847. },
  848. notification: {
  849. create: function (details) {
  850. const defaultDetails = {
  851. title: '',
  852. text: '',
  853. image: '',
  854. timeout: 5000,
  855. onclick: null,
  856. ondone: null,
  857. silent: false,
  858. };
  859.  
  860. try {
  861. return GMCompat.notification({ ...defaultDetails, ...details });
  862. } catch (error) {
  863. utils.logger.error('Failed to create notification:', error);
  864. return null;
  865. }
  866. },
  867.  
  868. close: function (id) {
  869. try {
  870. if (typeof GM_notification !== 'undefined' && GM_notification.close) {
  871. GM_notification.close(id);
  872. return true;
  873. }
  874. return false;
  875. } catch (error) {
  876. utils.logger.error('Failed to close notification:', error);
  877. return false;
  878. }
  879. },
  880. },
  881. request: {
  882. abort: function (requestId) {
  883. try {
  884. if (typeof GM_xmlhttpRequest !== 'undefined' && GM_xmlhttpRequest.abort) {
  885. GM_xmlhttpRequest.abort(requestId);
  886. return true;
  887. }
  888. return false;
  889. } catch (error) {
  890. utils.logger.error('Failed to abort request:', error);
  891. return false;
  892. }
  893. },
  894. },
  895. cookie: {
  896. list: async function (details = {}) {
  897. try {
  898. if (typeof GM_cookie !== 'undefined' && GM_cookie.list) {
  899. return await GM_cookie.list(details);
  900. }
  901. return [];
  902. } catch (error) {
  903. utils.logger.error('Failed to list cookies:', error);
  904. return [];
  905. }
  906. },
  907.  
  908. deleteAll: async function (details = {}) {
  909. try {
  910. if (typeof GM_cookie !== 'undefined' && GM_cookie.deleteAll) {
  911. return await GM_cookie.deleteAll(details);
  912. }
  913. return false;
  914. } catch (error) {
  915. utils.logger.error('Failed to delete all cookies:', error);
  916. return false;
  917. }
  918. },
  919. },
  920. };
  921. const exportGMCompat = function () {
  922. try {
  923. const target = !utils.isUndefined(unsafeWindow) ? unsafeWindow : window;
  924. Object.defineProperty(target, 'GMCompat', {
  925. value: GMCompat,
  926. writable: false,
  927. configurable: false,
  928. enumerable: true,
  929. });
  930. if (window.onurlchange !== undefined) {
  931. window.addEventListener('urlchange', () => {});
  932. }
  933. } catch (error) {
  934. console.error('Failed to export GMCompat:', error);
  935. }
  936. };
  937. exportGMCompat();
  938. })();