UserScript Compatibility Library

A library to ensure compatibility between different userscript managers

Version au 05/12/2024. Voir la dernière version.

Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greatest.deepsurf.us/scripts/519877/1497523/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://greatest.deepsurf.us/vi/users/1195312-renji-yuusei
// @version      1.5.0
// @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
// ==/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);
				}
			},
		},
	};

	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();
})();