A library to ensure compatibility between different userscript managers
Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greasyfork.org/scripts/519877/1507987/UserScript%20Compatibility%20Library.js
// ==UserScript== // @name UserScript Compatibility Library // @name:en UserScript Compatibility Library // @name:zh-CN UserScript 兼容库 // @name:ru Библиотека совместимости для пользовательских скриптов // @name:vi Thư viện tương thích cho userscript // @namespace https://greasyfork.org/vi/users/1195312-renji-yuusei // @version 2024.12.23.1 // @description A library to ensure compatibility between different userscript managers // @description:en A library to ensure compatibility between different userscript managers // @description:zh-CN 确保不同用户脚本管理器之间兼容性的库 // @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 // @description:ru Библиотека для обеспечения совместимости между различными менеджерами пользовательских скриптов // @author Yuusei // @license GPL-3.0-only // @grant unsafeWindow // @grant GM_info // @grant GM.info // @grant GM_getValue // @grant GM.getValue // @grant GM_setValue // @grant GM.setValue // @grant GM_deleteValue // @grant GM.deleteValue // @grant GM_listValues // @grant GM.listValues // @grant GM_xmlhttpRequest // @grant GM.xmlHttpRequest // @grant GM_download // @grant GM.download // @grant GM_notification // @grant GM.notification // @grant GM_addStyle // @grant GM.addStyle // @grant GM_registerMenuCommand // @grant GM.registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM.unregisterMenuCommand // @grant GM_setClipboard // @grant GM.setClipboard // @grant GM_getResourceText // @grant GM.getResourceText // @grant GM_getResourceURL // @grant GM.getResourceURL // @grant GM_openInTab // @grant GM.openInTab // @grant GM_addElement // @grant GM.addElement // @grant GM_addValueChangeListener // @grant GM.addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM.removeValueChangeListener // @grant GM_log // @grant GM.log // @grant GM_getTab // @grant GM.getTab // @grant GM_saveTab // @grant GM.saveTab // @grant GM_getTabs // @grant GM.getTabs // @grant GM_cookie // @grant GM.cookie // @grant GM_webRequest // @grant GM.webRequest // @grant GM_fetch // @grant GM.fetch // @grant window.close // @grant window.focus // @grant window.onurlchange // @grant GM_addValueChangeListener // @grant GM_removeValueChangeListener // @grant GM_getResourceURL // @grant GM_notification // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_unregisterMenuCommand // @grant GM_setClipboard // @grant GM_getResourceText // @grant GM_addStyle // @grant GM_download // @grant GM_cookie.get // @grant GM_cookie.set // @grant GM_cookie.delete // @grant GM_webRequest.listen // @grant GM_webRequest.onBeforeRequest // @grant GM_addElement.byTag // @grant GM_addElement.byId // @grant GM_addElement.byClass // @grant GM_addElement.byXPath // @grant GM_addElement.bySelector // @grant GM_removeElement // @grant GM_removeElements // @grant GM_getElement // @grant GM_getElements // @grant GM_addScript // @grant GM_removeScript // @grant GM_addLink // @grant GM_removeLink // @grant GM_addMeta // @grant GM_removeMeta // @grant GM_addIframe // @grant GM_removeIframe // @grant GM_addImage // @grant GM_removeImage // @grant GM_addVideo // @grant GM_removeVideo // @grant GM_addAudio // @grant GM_removeAudio // @grant GM_addCanvas // @grant GM_removeCanvas // @grant GM_addSvg // @grant GM_removeSvg // @grant GM_addObject // @grant GM_removeObject // @grant GM_addEmbed // @grant GM_removeEmbed // @grant GM_addApplet // @grant GM_removeApplet // @run-at document-start // @license GPL-3.0-only // @grant GM_addValueChangeListener.remove // @grant GM_getResourceURL.blob // @grant GM_notification.close // @grant GM_openInTab.focus // @grant GM_setClipboard.format // @grant GM_xmlhttpRequest.abort // @grant GM_download.progress // @grant GM_cookie.list // @grant GM_cookie.deleteAll // @grant GM_webRequest.filter // @grant GM_addElement.create // @grant GM_removeElement.all // @grant GM_getElement.all // @grant GM_addScript.remote // @grant GM_addLink.stylesheet // @grant GM_addMeta.viewport // @grant GM_addIframe.sandbox // @grant GM_addImage.lazy // @grant GM_addVideo.controls // @grant GM_addAudio.autoplay // @grant GM_addCanvas.context // @grant GM_addSvg.namespace // @grant GM_addObject.data // @grant GM_addEmbed.type // @grant GM_addApplet.code // ==/UserScript== (function () { 'use strict'; const utils = { isFunction: function (fn) { return typeof fn === 'function'; }, isUndefined: function (value) { return typeof value === 'undefined'; }, isObject: function (value) { return value !== null && typeof value === 'object'; }, sleep: function (ms) { return new Promise(resolve => setTimeout(resolve, ms)); }, retry: async function (fn, attempts = 3, delay = 1000) { let lastError; for (let i = 0; i < attempts; i++) { try { return await fn(); } catch (error) { lastError = error; if (i === attempts - 1) break; await this.sleep(delay * Math.pow(2, i)); } } throw lastError; }, debounce: function (fn, wait) { let timeout; return function (...args) { clearTimeout(timeout); timeout = setTimeout(() => fn.apply(this, args), wait); }; }, throttle: function (fn, limit) { let timeout; let inThrottle; return function (...args) { if (!inThrottle) { fn.apply(this, args); inThrottle = true; clearTimeout(timeout); timeout = setTimeout(() => (inThrottle = false), limit); } }; }, // Thêm các tiện ích mới isArray: function (arr) { return Array.isArray(arr); }, isString: function (str) { return typeof str === 'string'; }, isNumber: function (num) { return typeof num === 'number' && !isNaN(num); }, isBoolean: function (bool) { return typeof bool === 'boolean'; }, isNull: function (value) { return value === null; }, isEmpty: function (value) { if (this.isArray(value)) return value.length === 0; if (this.isObject(value)) return Object.keys(value).length === 0; if (this.isString(value)) return value.trim().length === 0; return false; }, }; const GMCompat = { info: (function () { if (!utils.isUndefined(GM_info)) return GM_info; if (!utils.isUndefined(GM) && GM.info) return GM.info; return {}; })(), storageCache: new Map(), cacheTimestamps: new Map(), cacheExpiry: 3600000, // 1 hour getValue: async function (key, defaultValue) { try { if (this.storageCache.has(key)) { const timestamp = this.cacheTimestamps.get(key); if (Date.now() - timestamp < this.cacheExpiry) { return this.storageCache.get(key); } } let value; if (!utils.isUndefined(GM_getValue)) { value = GM_getValue(key, defaultValue); } else if (!utils.isUndefined(GM) && GM.getValue) { value = await GM.getValue(key, defaultValue); } else { value = defaultValue; } this.storageCache.set(key, value); this.cacheTimestamps.set(key, Date.now()); return value; } catch (error) { console.error('getValue error:', error); return defaultValue; } }, setValue: async function (key, value) { try { this.storageCache.set(key, value); this.cacheTimestamps.set(key, Date.now()); if (!utils.isUndefined(GM_setValue)) { return GM_setValue(key, value); } if (!utils.isUndefined(GM) && GM.setValue) { return await GM.setValue(key, value); } } catch (error) { this.storageCache.delete(key); this.cacheTimestamps.delete(key); throw new Error('Failed to set value: ' + error.message); } }, deleteValue: async function (key) { try { this.storageCache.delete(key); this.cacheTimestamps.delete(key); if (!utils.isUndefined(GM_deleteValue)) { return GM_deleteValue(key); } if (!utils.isUndefined(GM) && GM.deleteValue) { return await GM.deleteValue(key); } } catch (error) { throw new Error('Failed to delete value: ' + error.message); } }, requestQueue: [], processingRequest: false, maxRetries: 3, retryDelay: 1000, xmlHttpRequest: async function (details) { const makeRequest = () => { return new Promise((resolve, reject) => { try { const callbacks = { onload: resolve, onerror: reject, ontimeout: reject, onprogress: details.onprogress, onreadystatechange: details.onreadystatechange, }; const finalDetails = { timeout: 30000, ...details, ...callbacks, }; if (!utils.isUndefined(GM_xmlhttpRequest)) { GM_xmlhttpRequest(finalDetails); } else if (!utils.isUndefined(GM) && GM.xmlHttpRequest) { GM.xmlHttpRequest(finalDetails); } else if (!utils.isUndefined(GM_fetch)) { GM_fetch(finalDetails.url, finalDetails); } else if (!utils.isUndefined(GM) && GM.fetch) { GM.fetch(finalDetails.url, finalDetails); } else { reject(new Error('XMLHttpRequest API not available')); } } catch (error) { reject(error); } }); }; return utils.retry(makeRequest, this.maxRetries, this.retryDelay); }, download: async function (details) { try { const downloadWithProgress = { ...details, onprogress: details.onprogress, onerror: details.onerror, onload: details.onload, }; if (!utils.isUndefined(GM_download)) { return new Promise((resolve, reject) => { GM_download({ ...downloadWithProgress, onload: resolve, onerror: reject, }); }); } if (!utils.isUndefined(GM) && GM.download) { return await GM.download(downloadWithProgress); } throw new Error('Download API not available'); } catch (error) { throw new Error('Download failed: ' + error.message); } }, notification: function (details) { return new Promise((resolve, reject) => { try { const defaultOptions = { timeout: 5000, highlight: false, silent: false, requireInteraction: false, priority: 0, }; const callbacks = { onclick: utils.debounce((...args) => { if (details.onclick) details.onclick(...args); resolve('clicked'); }, 300), ondone: (...args) => { if (details.ondone) details.ondone(...args); resolve('closed'); }, onerror: (...args) => { if (details.onerror) details.onerror(...args); reject('error'); }, }; const finalDetails = { ...defaultOptions, ...details, ...callbacks }; if (!utils.isUndefined(GM_notification)) { GM_notification(finalDetails); } else if (!utils.isUndefined(GM) && GM.notification) { GM.notification(finalDetails); } else { if ('Notification' in window) { Notification.requestPermission().then(permission => { if (permission === 'granted') { const notification = new Notification(finalDetails.title, { body: finalDetails.text, silent: finalDetails.silent, icon: finalDetails.image, tag: finalDetails.tag, requireInteraction: finalDetails.requireInteraction, badge: finalDetails.badge, vibrate: finalDetails.vibrate, }); notification.onclick = callbacks.onclick; notification.onerror = callbacks.onerror; if (finalDetails.timeout > 0) { setTimeout(() => { notification.close(); callbacks.ondone(); }, finalDetails.timeout); } } else { reject(new Error('Notification permission denied')); } }); } else { reject(new Error('Notification API not available')); } } } catch (error) { reject(error); } }); }, addStyle: function (css) { try { const testStyle = document.createElement('style'); testStyle.textContent = css; if (testStyle.sheet === null) { throw new Error('Invalid CSS'); } if (!utils.isUndefined(GM_addStyle)) { return GM_addStyle(css); } if (!utils.isUndefined(GM) && GM.addStyle) { return GM.addStyle(css); } const style = document.createElement('style'); style.textContent = css; style.type = 'text/css'; document.head.appendChild(style); return style; } catch (error) { throw new Error('Failed to add style: ' + error.message); } }, registerMenuCommand: function (name, fn, accessKey) { try { if (!utils.isFunction(fn)) { throw new Error('Command callback must be a function'); } if (!utils.isUndefined(GM_registerMenuCommand)) { return GM_registerMenuCommand(name, fn, accessKey); } if (!utils.isUndefined(GM) && GM.registerMenuCommand) { return GM.registerMenuCommand(name, fn, accessKey); } } catch (error) { throw new Error('Failed to register menu command: ' + error.message); } }, setClipboard: function (text, info) { try { if (!utils.isUndefined(GM_setClipboard)) { return GM_setClipboard(text, info); } if (!utils.isUndefined(GM) && GM.setClipboard) { return GM.setClipboard(text, info); } return navigator.clipboard.writeText(text); } catch (error) { throw new Error('Failed to set clipboard: ' + error.message); } }, getResourceText: async function (name) { try { if (!utils.isUndefined(GM_getResourceText)) { return GM_getResourceText(name); } if (!utils.isUndefined(GM) && GM.getResourceText) { return await GM.getResourceText(name); } throw new Error('Resource API not available'); } catch (error) { throw new Error('Failed to get resource text: ' + error.message); } }, getResourceURL: async function (name) { try { if (!utils.isUndefined(GM_getResourceURL)) { return GM_getResourceURL(name); } if (!utils.isUndefined(GM) && GM.getResourceURL) { return await GM.getResourceURL(name); } throw new Error('Resource URL API not available'); } catch (error) { throw new Error('Failed to get resource URL: ' + error.message); } }, openInTab: function (url, options = {}) { try { const defaultOptions = { active: true, insert: true, setParent: true, }; const finalOptions = { ...defaultOptions, ...options }; if (!utils.isUndefined(GM_openInTab)) { return GM_openInTab(url, finalOptions); } if (!utils.isUndefined(GM) && GM.openInTab) { return GM.openInTab(url, finalOptions); } return window.open(url, '_blank'); } catch (error) { throw new Error('Failed to open tab: ' + error.message); } }, cookie: { get: async function (details) { try { if (!utils.isUndefined(GM_cookie) && GM_cookie.get) { return await GM_cookie.get(details); } if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.get) { return await GM.cookie.get(details); } return document.cookie; } catch (error) { throw new Error('Failed to get cookie: ' + error.message); } }, set: async function (details) { try { if (!utils.isUndefined(GM_cookie) && GM_cookie.set) { return await GM_cookie.set(details); } if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.set) { return await GM.cookie.set(details); } document.cookie = details; } catch (error) { throw new Error('Failed to set cookie: ' + error.message); } }, delete: async function (details) { try { if (!utils.isUndefined(GM_cookie) && GM_cookie.delete) { return await GM_cookie.delete(details); } if (!utils.isUndefined(GM) && GM.cookie && GM.cookie.delete) { return await GM.cookie.delete(details); } } catch (error) { throw new Error('Failed to delete cookie: ' + error.message); } }, }, webRequest: { listen: function (filter, callback) { try { if (!utils.isUndefined(GM_webRequest) && GM_webRequest.listen) { return GM_webRequest.listen(filter, callback); } if (!utils.isUndefined(GM) && GM.webRequest && GM.webRequest.listen) { return GM.webRequest.listen(filter, callback); } } catch (error) { throw new Error('Failed to listen to web request: ' + error.message); } }, onBeforeRequest: function (filter, callback) { try { if (!utils.isUndefined(GM_webRequest) && GM_webRequest.onBeforeRequest) { return GM_webRequest.onBeforeRequest(filter, callback); } if (!utils.isUndefined(GM) && GM.webRequest && GM.webRequest.onBeforeRequest) { return GM.webRequest.onBeforeRequest(filter, callback); } } catch (error) { throw new Error('Failed to handle onBeforeRequest: ' + error.message); } }, }, dom: { addElement: function (tag, attributes = {}, parent = document.body) { try { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { element.setAttribute(key, value); }); parent.appendChild(element); return element; } catch (error) { throw new Error('Failed to add element: ' + error.message); } }, removeElement: function (element) { try { if (element && element.parentNode) { element.parentNode.removeChild(element); } } catch (error) { throw new Error('Failed to remove element: ' + error.message); } }, getElement: function (selector) { try { return document.querySelector(selector); } catch (error) { throw new Error('Failed to get element: ' + error.message); } }, getElements: function (selector) { try { return Array.from(document.querySelectorAll(selector)); } catch (error) { throw new Error('Failed to get elements: ' + error.message); } }, }, storage: { setObject: async function (key, obj) { try { const jsonStr = JSON.stringify(obj); return await GMCompat.setValue(key, jsonStr); } catch (error) { utils.logger.error('setObject failed:', error); throw error; } }, getObject: async function (key, defaultValue = null) { try { const jsonStr = await GMCompat.getValue(key, null); return jsonStr ? JSON.parse(jsonStr) : defaultValue; } catch (error) { utils.logger.error('getObject failed:', error); return defaultValue; } }, appendToArray: async function (key, value) { try { const arr = await this.getObject(key, []); arr.push(value); await this.setObject(key, arr); return arr; } catch (error) { utils.logger.error('appendToArray failed:', error); throw error; } }, removeFromArray: async function (key, predicate) { try { const arr = await this.getObject(key, []); const filtered = arr.filter(item => !predicate(item)); await this.setObject(key, filtered); return filtered; } catch (error) { utils.logger.error('removeFromArray failed:', error); throw error; } }, }, request: { downloadWithProgress: async function (url, filename, onProgress) { try { return await GMCompat.download({ url: url, name: filename, onprogress: onProgress, saveAs: true, }); } catch (error) { utils.logger.error('downloadWithProgress failed:', error); throw error; } }, fetchWithRetry: async function (url, options = {}) { const finalOptions = { method: 'GET', timeout: 10000, retry: 3, retryDelay: 1000, ...options, }; return await utils.retry( async () => { return await GMCompat.xmlHttpRequest({ url: url, ...finalOptions, }); }, finalOptions.retry, finalOptions.retryDelay ); }, }, ui: { createMenuCommand: function (name, callback, options = {}) { const defaultOptions = { accessKey: null, autoClose: true, }; const finalOptions = { ...defaultOptions, ...options }; return GMCompat.registerMenuCommand( name, async (...args) => { try { await callback(...args); if (finalOptions.autoClose) { window.close(); } } catch (error) { utils.logger.error('Menu command failed:', error); GMCompat.notification({ title: 'Lỗi', text: `Lỗi thực thi lệnh: ${error.message}`, type: 'error', }); } }, finalOptions.accessKey ); }, toast: function (message, type = 'info', duration = 3000) { return GMCompat.notification({ title: type.charAt(0).toUpperCase() + type.slice(1), text: message, timeout: duration, onclick: () => {}, silent: true, highlight: false, }); }, }, clipboard: { copyFormatted: async function (text, format = 'text/plain') { try { await GMCompat.setClipboard(text, format); return true; } catch (error) { utils.logger.error('copyFormatted failed:', error); return false; } }, copyHTML: async function (html) { return await this.copyFormatted(html, 'text/html'); }, }, cookies: { getAll: async function (domain) { try { return await GMCompat.cookie.get({ domain: domain }); } catch (error) { utils.logger.error('getAll cookies failed:', error); return []; } }, setCookie: async function (name, value, options = {}) { try { const defaultOptions = { path: '/', secure: true, sameSite: 'Lax', expirationDate: Math.floor(Date.now() / 1000) + 86400, // 1 day }; await GMCompat.cookie.set({ name: name, value: value, ...defaultOptions, ...options, }); } catch (error) { utils.logger.error('setCookie failed:', error); throw error; } }, }, valueChangeListener: { listeners: new Map(), add: function (name, callback) { try { if (typeof GM_addValueChangeListener !== 'undefined') { const listenerId = GM_addValueChangeListener(name, callback); this.listeners.set(name, listenerId); return listenerId; } return null; } catch (error) { utils.logger.error('Failed to add value change listener:', error); return null; } }, remove: function (name) { try { const listenerId = this.listeners.get(name); if (listenerId && typeof GM_removeValueChangeListener !== 'undefined') { GM_removeValueChangeListener(listenerId); this.listeners.delete(name); return true; } return false; } catch (error) { utils.logger.error('Failed to remove value change listener:', error); return false; } }, }, resource: { getBlob: async function (name) { try { const url = await GMCompat.getResourceURL(name); const response = await fetch(url); return await response.blob(); } catch (error) { utils.logger.error('Failed to get resource blob:', error); throw error; } }, getText: async function (name, defaultValue = '') { try { return (await GMCompat.getResourceText(name)) || defaultValue; } catch (error) { utils.logger.error('Failed to get resource text:', error); return defaultValue; } }, }, tab: { open: function (url, options = {}) { const defaultOptions = { active: true, insert: true, setParent: true, incognito: false, }; try { return GMCompat.openInTab(url, { ...defaultOptions, ...options }); } catch (error) { utils.logger.error('Failed to open tab:', error); return null; } }, focus: function (tab) { try { if (tab && tab.focus) { tab.focus(); return true; } return false; } catch (error) { utils.logger.error('Failed to focus tab:', error); return false; } }, }, notification: { create: function (details) { const defaultDetails = { title: '', text: '', image: '', timeout: 5000, onclick: null, ondone: null, silent: false, }; try { return GMCompat.notification({ ...defaultDetails, ...details }); } catch (error) { utils.logger.error('Failed to create notification:', error); return null; } }, close: function (id) { try { if (typeof GM_notification !== 'undefined' && GM_notification.close) { GM_notification.close(id); return true; } return false; } catch (error) { utils.logger.error('Failed to close notification:', error); return false; } }, }, request: { abort: function (requestId) { try { if (typeof GM_xmlhttpRequest !== 'undefined' && GM_xmlhttpRequest.abort) { GM_xmlhttpRequest.abort(requestId); return true; } return false; } catch (error) { utils.logger.error('Failed to abort request:', error); return false; } }, }, cookie: { list: async function (details = {}) { try { if (typeof GM_cookie !== 'undefined' && GM_cookie.list) { return await GM_cookie.list(details); } return []; } catch (error) { utils.logger.error('Failed to list cookies:', error); return []; } }, deleteAll: async function (details = {}) { try { if (typeof GM_cookie !== 'undefined' && GM_cookie.deleteAll) { return await GM_cookie.deleteAll(details); } return false; } catch (error) { utils.logger.error('Failed to delete all cookies:', error); return false; } }, }, }; const exportGMCompat = function () { try { const target = !utils.isUndefined(unsafeWindow) ? unsafeWindow : window; Object.defineProperty(target, 'GMCompat', { value: GMCompat, writable: false, configurable: false, enumerable: true, }); if (window.onurlchange !== undefined) { window.addEventListener('urlchange', () => {}); } } catch (error) { console.error('Failed to export GMCompat:', error); } }; exportGMCompat(); })();