Greasy Fork is available in English.

Greasyfork 快捷编辑收藏

在GF脚本页添加快速打开收藏集编辑页面功能

Από την 08/02/2022. Δείτε την τελευταία έκδοση.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey, το Greasemonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Violentmonkey για να εγκαταστήσετε αυτόν τον κώδικα.

θα χρειαστεί να εγκαταστήσετε μια επέκταση όπως το Tampermonkey ή το Userscripts για να εγκαταστήσετε αυτόν τον κώδικα.

You will need to install an extension such as Tampermonkey to install this script.

Θα χρειαστεί να εγκαταστήσετε μια επέκταση διαχείρισης κώδικα χρήστη για να εγκαταστήσετε αυτόν τον κώδικα.

(Έχω ήδη έναν διαχειριστή κώδικα χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Έχω ήδη έναν διαχειριστή στυλ χρήστη, επιτρέψτε μου να τον εγκαταστήσω!)

/* eslint-disable no-multi-spaces */

// ==UserScript==
// @name               Greasyfork 快捷编辑收藏
// @name:zh-CN         Greasyfork 快捷编辑收藏
// @name:zh-TW         Greasyfork 快捷編輯收藏
// @name:en            Greasyfork script-set-edit button
// @namespace          Greasyfork-Favorite
// @version            0.1
// @description        在GF脚本页添加快速打开收藏集编辑页面功能
// @description:zh-CN  在GF脚本页添加快速打开收藏集编辑页面功能
// @description:zh-TW  在GF腳本頁添加快速打開收藏集編輯頁面功能
// @description:en     Add open script-set-edit-page button in GF script page
// @author             PY-DNG
// @license            GPL-3
// @match              http*://greasyfork.org/*
// @icon               https://api.iowen.cn/favicon/get.php?url=greasyfork.org
// @grant              GM_xmlhttpRequest
// @grant              GM_setValue
// @grant              GM_getValue
// ==/UserScript==

(function() {
    'use strict';

    // Arguments: level=LogLevel.Info, logContent, asObject=false
    // Needs one call "DoLog();" to get it initialized before using it!
    function DoLog() {
        // Global log levels set
        window.LogLevel = {
            None: 0,
            Error: 1,
            Success: 2,
            Warning: 3,
            Info: 4,
        }
        window.LogLevelMap = {};
        window.LogLevelMap[LogLevel.None]     = {prefix: ''          , color: 'color:#ffffff'}
        window.LogLevelMap[LogLevel.Error]    = {prefix: '[Error]'   , color: 'color:#ff0000'}
        window.LogLevelMap[LogLevel.Success]  = {prefix: '[Success]' , color: 'color:#00aa00'}
        window.LogLevelMap[LogLevel.Warning]  = {prefix: '[Warning]' , color: 'color:#ffa500'}
        window.LogLevelMap[LogLevel.Info]     = {prefix: '[Info]'    , color: 'color:#888888'}
        window.LogLevelMap[LogLevel.Elements] = {prefix: '[Elements]', color: 'color:#000000'}

        // Current log level
        DoLog.logLevel = (unsafeWindow ? unsafeWindow.isPY_DNG : window.isPY_DNG) ? LogLevel.Info : LogLevel.Warning; // Info Warning Success Error

        // Log counter
        DoLog.logCount === undefined && (DoLog.logCount = 0);
        if (++DoLog.logCount > 512) {
            console.clear();
            DoLog.logCount = 0;
        }

        // Get args
        let level, logContent, asObject;
        switch (arguments.length) {
            case 1:
                level = LogLevel.Info;
                logContent = arguments[0];
                asObject = false;
                break;
            case 2:
                level = arguments[0];
                logContent = arguments[1];
                asObject = false;
                break;
            case 3:
                level = arguments[0];
                logContent = arguments[1];
                asObject = arguments[2];
                break;
            default:
                level = LogLevel.Info;
                logContent = 'DoLog initialized.';
                asObject = false;
                break;
        }

        // Log when log level permits
        if (level <= DoLog.logLevel) {
            let msg = '%c' + LogLevelMap[level].prefix;
            let subst = LogLevelMap[level].color;

            if (asObject) {
                msg += ' %o';
            } else {
                switch(typeof(logContent)) {
                    case 'string': msg += ' %s'; break;
                    case 'number': msg += ' %d'; break;
                    case 'object': msg += ' %o'; break;
                }
            }

            console.log(msg, subst, logContent);
        }
    }
    DoLog();

	bypassXB();
	GM_PolyFill('default');

	// Inner consts with i18n
	const CONST = {
		Text: {
			'zh-CN': {
				FavEdit: '收藏集:',
				Edit: '编辑',
				CopySID: '复制脚本ID'
			},
			'zh-TW': {
				FavEdit: '收藏集:',
				Edit: '編輯',
				CopySID: '複製腳本ID'
			},
			'en': {
				FavEdit: 'Add to/Remove from favorite list: ',
				Edit: 'Edit',
				CopySID: 'Copy-Script-ID'
			},
			'default': {
				FavEdit: 'Add to/Remove from favorite list: ',
				Edit: 'Edit',
				CopySID: 'Copy-Script-ID'
			},
		}
	}

	// Get i18n code
	let i18n = navigator.language;
	if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}

	main()
	function main() {
		const HOST = getHost();
		const API = getAPI();

		// Common actions
		commons();

		// API-based actions
		switch(API[1]) {
			case "scripts":
				centerScript(API);
				break;
			default:
				DoLog('API is {}'.replace('{}', API));
		}
	}

	function centerScript(API) {
		switch(API[3]) {
			case undefined:
				pageScript();
				break;
			case 'code':
				pageCode();
				break;
			case 'feedback':
				pageFeedback();
				break;
		}
	}

	function commons() {
		// Your common actions here...
	}

	function pageScript() {
		addFavPanel();
	}

	function pageCode() {
		addFavPanel();
	}

	function pageFeedback() {
		addFavPanel();
	}

	function addFavPanel() {
		if (!getUserpage()) {return false;}
		GUI();

		function GUI() {
			// Get elements
			const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');
			const script_parent = script_after.parentElement;

			// My elements
			const script_favorite = $C('div');
			script_favorite.id = 'script-favorite';
			script_favorite.style.margin = '0.75em 0';
			script_favorite.innerHTML = CONST.Text[i18n].FavEdit;

			const favorite_groups = $C('select');
			favorite_groups.id = 'favorite-groups';

			const stored_sets = GM_getValue('script-sets', {sets: []}).sets;
			for (const set of stored_sets) {
				// Make <option>
				const option = $C('option');
				option.innerText = set.name;
				option.value = set.linkedit;
				$A(favorite_groups, option);
			}

			getScriptSets(function(sets) {
				clearChildnodes(favorite_groups);
				for (const set of sets) {
					// Make <option>
					const option = $C('option');
					option.innerText = set.name;
					option.value = set.linkedit;
					$A(favorite_groups, option);
				}

				// Set edit-button.href
				favorite_edit.href = favorite_groups.value;
			})
			favorite_groups.addEventListener('change', function(e) {
				favorite_edit.href = favorite_groups.value;
			});

			const favorite_edit = $C('a');
			favorite_edit.id = 'favorite-add';
			favorite_edit.innerHTML = CONST.Text[i18n].Edit;
			favorite_edit.style.margin = favorite_edit.style.margin = '0px 0.5em';
			favorite_edit.target = '_blank';

			const favorite_copy = $C('a');
			favorite_copy.id = 'favorite-copy';
			favorite_copy.href = 'javascript: void(0);';
			favorite_copy.innerHTML = CONST.Text[i18n].CopySID;
			favorite_copy.addEventListener('click', function() {
				copyText(getStrSID());
			});

			// Append to document
			$A(script_favorite, favorite_groups);
			$I(script_parent, script_favorite, script_after);
			$A(script_favorite, favorite_edit);
			$A(script_favorite, favorite_copy);
		}
	}

	function getScriptSets(callback, args=[]) {
		const userpage = getUserpage();
		getDocument(userpage, function(oDom) {
			const user_script_sets = oDom.querySelector('#user-script-sets');
			const script_sets = [];

			for (const li of user_script_sets.querySelectorAll('li')) {
				// Get fav info
				const name = li.childNodes[0].nodeValue.trimRight();
				const link = li.children[0].href;
				const linkedit = li.children[1].href;

				// Append to script_sets
				script_sets.push({
					name: name,
					link: link,
					linkedit: linkedit
				});
			}

			// Save to GM_storage
			GM_setValue('script-sets', {
				sets: script_sets,
				time: (new Date()).getTime(),
				version: '0.1'
			});

			// callback
			callback.apply(null, [script_sets].concat(args));
		});
	}

	function getUserpage() {
		const a = $('#nav-user-info>.user-profile-link>a');
		return a ? a.href : null;
	}

	function getStrSID(url=location.href) {
		const API = getAPI(url);
		const strSID = API[2].match(/\d+/);
		return strSID;
	}

	function getSID(url=location.href) {
		return Number(getStrSID(url));
	}

	function $(e) {return document.querySelector(e);}
	function $C(e) {return document.createElement(e);}
	function $A(a,b) {return a.appendChild(b);}
	function $I(a,b,c) {return a.insertBefore(b,c);}

	// Remove all childnodes from an element
	function clearChildnodes(element) {
		const cns = []
		for (const cn of element.childNodes) {
			cns.push(cn);
		}
		for (const cn of cns) {
			element.removeChild(cn);
		}
	}

	// Just stopPropagation and preventDefault
	function destroyEvent(e) {
		if (!e) {return false;};
		if (!e instanceof Event) {return false;};
		e.stopPropagation();
		e.preventDefault();
	}

	// Download and parse a url page into a html document(dom).
    // when xhr onload: callback.apply([dom, args])
    function getDocument(url, callback, args=[]) {
        GM_xmlhttpRequest({
            method       : 'GET',
            url          : url,
            responseType : 'blob',
			onloadstart  : function() {
				DoLog(LogLevel.Info, 'getting document, url=\'' + url + '\'');
			},
            onload       : function(response) {
                const htmlblob = response.response;
				parseDocument(htmlblob, callback, args);
            }
        })
    }

	function parseDocument(htmlblob, callback, args=[]) {
		const reader = new FileReader();
		reader.onload = function(e) {
			const htmlText = reader.result;
			const dom = new DOMParser().parseFromString(htmlText, 'text/html');
			args = [dom].concat(args);
			callback.apply(null, args);
			//callback(dom, htmlText);
		}
		reader.readAsText(htmlblob, document.characterSet);
	}

	// GM_XHR HOOK: The number of running GM_XHRs in a time must under maxXHR
	// Returns the abort function to stop the request anyway(no matter it's still waiting, or requesting)
	// (If the request is invalid, such as url === '', will return false and will NOT make this request)
	// If the abort function called on a request that is not running(still waiting or finished), there will be NO onabort event
	// Requires: function delItem(){...} & function uniqueIDMaker(){...}
	function GMXHRHook(maxXHR=5) {
		const GM_XHR = GM_xmlhttpRequest;
		const getID = uniqueIDMaker();
		let todoList = [], ongoingList = [];
		GM_xmlhttpRequest = safeGMxhr;

		function safeGMxhr() {
			// Get an id for this request, arrange a request object for it.
			const id = getID();
			const request = {id: id, args: arguments, aborter: null};

			// Deal onload function first
			dealEndingEvents(request);

			/* DO NOT DO THIS! KEEP ITS ORIGINAL PROPERTIES!
			// Stop invalid requests
			if (!validCheck(request)) {
				return false;
			}
			*/

			// Judge if we could start the request now or later?
			todoList.push(request);
			checkXHR();
			return makeAbortFunc(id);

			// Decrease activeXHRCount while GM_XHR onload;
			function dealEndingEvents(request) {
				const e = request.args[0];

				// onload event
				const oriOnload = e.onload;
				e.onload = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnload ? oriOnload.apply(null, arguments) : function() {};
				}

				// onerror event
				const oriOnerror = e.onerror;
				e.onerror = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnerror ? oriOnerror.apply(null, arguments) : function() {};
				}

				// ontimeout event
				const oriOntimeout = e.ontimeout;
				e.ontimeout = function() {
					reqFinish(request.id);
					checkXHR();
					oriOntimeout ? oriOntimeout.apply(null, arguments) : function() {};
				}

				// onabort event
				const oriOnabort = e.onabort;
				e.onabort = function() {
					reqFinish(request.id);
					checkXHR();
					oriOnabort ? oriOnabort.apply(null, arguments) : function() {};
				}
			}

			// Check if the request is invalid
			function validCheck(request) {
				const e = request.args[0];

				if (!e.url) {
					return false;
				}

				return true;
			}

			// Call a XHR from todoList and push the request object to ongoingList if called
			function checkXHR() {
				if (ongoingList.length >= maxXHR) {return false;};
				if (todoList.length === 0) {return false;};
				const req = todoList.shift();
				const reqArgs = req.args;
				const aborter = GM_XHR.apply(null, reqArgs);
				req.aborter = aborter;
				ongoingList.push(req);
				return req;
			}

			// Make a function that aborts a certain request
			function makeAbortFunc(id) {
				return function() {
					let i;

					// Check if the request haven't been called
					for (i = 0; i < todoList.length; i++) {
						const req = todoList[i];
						if (req.id === id) {
							// found this request: haven't been called
							delItem(todoList, i);
							return true;
						}
					}

					// Check if the request is running now
					for (i = 0; i < ongoingList.length; i++) {
						const req = todoList[i];
						if (req.id === id) {
							// found this request: running now
							req.aborter();
							reqFinish(id);
							checkXHR();
						}
					}

					// Oh no, this request is already finished...
					return false;
				}
			}

			// Remove a certain request from ongoingList
			function reqFinish(id) {
				let i;
				for (i = 0; i < ongoingList.length; i++) {
					const req = ongoingList[i];
					if (req.id === id) {
						ongoingList = delItem(ongoingList, i);
						return true;
					}
				}
				return false;
			}
		}
	}

	// Get a url argument from lacation.href
	// also recieve a function to deal the matched string
	// returns defaultValue if name not found
    // Args: name, dealFunc=(function(a) {return a;}), defaultValue=null
	function getUrlArgv(details) {
        typeof(details) === 'string'    && (details = {name: details});
        typeof(details) === 'undefined' && (details = {});
        if (!details.name) {return null;};

        const url = details.url ? details.url : location.href;
        const name = details.name ? details.name : '';
        const dealFunc = details.dealFunc ? details.dealFunc : ((a)=>{return a;});
        const defaultValue = details.defaultValue ? details.defaultValue : null;
		const matcher = new RegExp(name + '=([^&]+)');
		const result = url.match(matcher);
		const argv = result ? dealFunc(result[1]) : defaultValue;

		return argv;
	}

	// Copy text to clipboard (needs to be called in an user event)
    function copyText(text) {
        // Create a new textarea for copying
        const newInput = document.createElement('textarea');
        document.body.appendChild(newInput);
        newInput.value = text;
        newInput.select();
        document.execCommand('copy');
        document.body.removeChild(newInput);
    }

	// Append a style text to document(<head>) with a <style> element
    function addStyle(css, id) {
		const style = document.createElement("style");
		id && (style.id = id);
		style.textContent = css;
		for (const elm of document.querySelectorAll('#'+id)) {
			elm.parentElement && elm.parentElement.removeChild(elm);
		}
        document.head.appendChild(style);
    }

	// File download function
	// details looks like the detail of GM_xmlhttpRequest
	// onload function will be called after file saved to disk
	function downloadFile(details) {
		if (!details.url || !details.name) {return false;};

		// Configure request object
		const requestObj = {
			url: details.url,
			responseType: 'blob',
			onload: function(e) {
				// Save file
				saveFile(URL.createObjectURL(e.response), details.name);

				// onload callback
				details.onload ? details.onload(e) : function() {};
			}
		}
		if (details.onloadstart       ) {requestObj.onloadstart        = details.onloadstart;};
		if (details.onprogress        ) {requestObj.onprogress         = details.onprogress;};
		if (details.onerror           ) {requestObj.onerror            = details.onerror;};
		if (details.onabort           ) {requestObj.onabort            = details.onabort;};
		if (details.onreadystatechange) {requestObj.onreadystatechange = details.onreadystatechange;};
		if (details.ontimeout         ) {requestObj.ontimeout          = details.ontimeout;};

		// Send request
		GM_xmlhttpRequest(requestObj);
	}

	// get '/' splited API array from a url
	function getAPI(url=location.href) {
		return url.replace(/https?:\/\/(.*?\.){1,2}.*?\//, '').replace(/\?.*/, '').match(/[^\/]+?(?=(\/|$))/g);
	}

	// get host part from a url(includes '^https://', '/$')
	function getHost(url=location.href) {
		const match = location.href.match(/https?:\/\/[^\/]+\//);
		return match ? match[0] : match;
	}

    // Your code here...
	// Bypass xbrowser's useless GM_functions
	function bypassXB() {
		if (typeof(mbrowser) === 'object') {
			window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined;
		}
	}

    // GM_Polyfill By PY-DNG
	// 2021.07.18 - 2021.07.19
	// Simply provides the following GM_functions using localStorage, XMLHttpRequest and window.open:
	// Returns object GM_POLYFILLED which has the following properties that shows you which GM_functions are actually polyfilled:
	// GM_setValue, GM_getValue, GM_deleteValue, GM_listValues, GM_xmlhttpRequest, GM_openInTab, GM_setClipboard, unsafeWindow(object)
	// All polyfilled GM_functions are accessable in window object/Global_Scope(only without Tempermonkey Sandboxing environment)
	function GM_PolyFill(name='default') {
		const GM_POLYFILL_KEY_STORAGE = 'GM_STORAGE_POLYFILL';
		let GM_POLYFILL_storage;
		const GM_POLYFILLED = {
			GM_setValue: true,
			GM_getValue: true,
			GM_deleteValue: true,
			GM_listValues: true,
			GM_xmlhttpRequest: true,
			GM_openInTab: true,
			GM_setClipboard: true,
			unsafeWindow: true,
			once: false
		}

		// Ignore GM_PolyFill_Once
		window.GM_POLYFILLED && window.GM_POLYFILLED.once && (window.unsafeWindow = window.GM_setClipboard = window.GM_openInTab = window.GM_xmlhttpRequest = window.GM_getValue = window.GM_setValue = window.GM_listValues = window.GM_deleteValue = undefined);

		GM_setValue_polyfill();
		GM_getValue_polyfill();
		GM_deleteValue_polyfill();
		GM_listValues_polyfill();
		GM_xmlhttpRequest_polyfill();
		GM_openInTab_polyfill();
		GM_setClipboard_polyfill();
		unsafeWindow_polyfill();

		function GM_POLYFILL_getStorage() {
			let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
			gstorage = gstorage ? JSON.parse(gstorage) : {};
			let storage = gstorage[name] ? gstorage[name] : {};
			return storage;
		}

		function GM_POLYFILL_saveStorage() {
			let gstorage = localStorage.getItem(GM_POLYFILL_KEY_STORAGE);
			gstorage = gstorage ? JSON.parse(gstorage) : {};
			gstorage[name] = GM_POLYFILL_storage;
			localStorage.setItem(GM_POLYFILL_KEY_STORAGE, JSON.stringify(gstorage));
		}

		// GM_setValue
		function GM_setValue_polyfill() {
			typeof (GM_setValue) === 'function' ? GM_POLYFILLED.GM_setValue = false: window.GM_setValue = PF_GM_setValue;;

			function PF_GM_setValue(name, value) {
				GM_POLYFILL_storage = GM_POLYFILL_getStorage();
				name = String(name);
				GM_POLYFILL_storage[name] = value;
				GM_POLYFILL_saveStorage();
			}
		}

		// GM_getValue
		function GM_getValue_polyfill() {
			typeof (GM_getValue) === 'function' ? GM_POLYFILLED.GM_getValue = false: window.GM_getValue = PF_GM_getValue;

			function PF_GM_getValue(name, defaultValue) {
				GM_POLYFILL_storage = GM_POLYFILL_getStorage();
				name = String(name);
				if (GM_POLYFILL_storage.hasOwnProperty(name)) {
					return GM_POLYFILL_storage[name];
				} else {
					return defaultValue;
				}
			}
		}

		// GM_deleteValue
		function GM_deleteValue_polyfill() {
			typeof (GM_deleteValue) === 'function' ? GM_POLYFILLED.GM_deleteValue = false: window.GM_deleteValue = PF_GM_deleteValue;

			function PF_GM_deleteValue(name) {
				GM_POLYFILL_storage = GM_POLYFILL_getStorage();
				name = String(name);
				if (GM_POLYFILL_storage.hasOwnProperty(name)) {
					delete GM_POLYFILL_storage[name];
					GM_POLYFILL_saveStorage();
				}
			}
		}

		// GM_listValues
		function GM_listValues_polyfill() {
			typeof (GM_listValues) === 'function' ? GM_POLYFILLED.GM_listValues = false: window.GM_listValues = PF_GM_listValues;

			function PF_GM_listValues() {
				GM_POLYFILL_storage = GM_POLYFILL_getStorage();
				return Object.keys(GM_POLYFILL_storage);
			}
		}

		// unsafeWindow
		function unsafeWindow_polyfill() {
			typeof (unsafeWindow) === 'object' ? GM_POLYFILLED.unsafeWindow = false: window.unsafeWindow = window;
		}

		// GM_xmlhttpRequest
		// not supported properties of details: synchronous binary nocache revalidate context fetch
		// not supported properties of response(onload arguments[0]): finalUrl
		// ---!IMPORTANT!--- DOES NOT SUPPORT CROSS-ORIGIN REQUESTS!!!!! ---!IMPORTANT!---
		function GM_xmlhttpRequest_polyfill() {
			typeof (GM_xmlhttpRequest) === 'function' ? GM_POLYFILLED.GM_xmlhttpRequest = false: window.GM_xmlhttpRequest = PF_GM_xmlhttpRequest;

			// details.synchronous is not supported as Tempermonkey
			function PF_GM_xmlhttpRequest(details) {
				const xhr = new XMLHttpRequest();

				// open request
				const openArgs = [details.method, details.url, true];
				if (details.user && details.password) {
					openArgs.push(details.user);
					openArgs.push(details.password);
				}
				xhr.open.apply(xhr, openArgs);

				// set headers
				if (details.headers) {
					for (const key of Object.keys(details.headers)) {
						xhr.setRequestHeader(key, details.headers[key]);
					}
				}
				details.cookie ? xhr.setRequestHeader('cookie', details.cookie) : function () {};
				details.anonymous ? xhr.setRequestHeader('cookie', '') : function () {};

				// properties
				xhr.timeout = details.timeout;
				xhr.responseType = details.responseType;
				details.overrideMimeType ? xhr.overrideMimeType(details.overrideMimeType) : function () {};

				// events
				xhr.onabort = details.onabort;
				xhr.onerror = details.onerror;
				xhr.onloadstart = details.onloadstart;
				xhr.onprogress = details.onprogress;
				xhr.onreadystatechange = details.onreadystatechange;
				xhr.ontimeout = details.ontimeout;
				xhr.onload = function (e) {
					const response = {
						readyState: xhr.readyState,
						status: xhr.status,
						statusText: xhr.statusText,
						responseHeaders: xhr.getAllResponseHeaders(),
						response: xhr.response
					};
					(details.responseType === '' || details.responseType === 'text') ? (response.responseText = xhr.responseText) : function () {};
					(details.responseType === '' || details.responseType === 'document') ? (response.responseXML = xhr.responseXML) : function () {};
					details.onload(response);
				}

				// send request
				details.data ? xhr.send(details.data) : xhr.send();

				return {
					abort: xhr.abort
				};
			}
		}

		// NOTE: options(arg2) is NOT SUPPORTED! if provided, then will just be skipped.
		function GM_openInTab_polyfill() {
			typeof (GM_openInTab) === 'function' ? GM_POLYFILLED.GM_openInTab = false: window.GM_openInTab = PF_GM_openInTab;

			function PF_GM_openInTab(url) {
				window.open(url);
			}
		}

		// NOTE: needs to be called in an event handler function, and info(arg2) is NOT SUPPORTED!
		function GM_setClipboard_polyfill() {
			typeof (GM_setClipboard) === 'function' ? GM_POLYFILLED.GM_setClipboard = false: window.GM_setClipboard = PF_GM_setClipboard;

			function PF_GM_setClipboard(text) {
				// Create a new textarea for copying
				const newInput = document.createElement('textarea');
				document.body.appendChild(newInput);
				newInput.value = text;
				newInput.select();
				document.execCommand('copy');
				document.body.removeChild(newInput);
			}
		}

		return GM_POLYFILLED;
	}

	// Makes a function that returns a unique ID number each time
	function uniqueIDMaker() {
		let id = 0;
		return makeID;
		function makeID() {
			id++;
			return id;
		}
	}

	// Del a item from an array using its index. Returns the array but can NOT modify the original array directly!!
	function delItem(arr, delIndex) {
		arr = arr.slice(0, delIndex).concat(arr.slice(delIndex+1));
		return arr;
	}
})();