返回首頁 

Greasy Fork is available in English.

Greasyfork Set Edit+

Ajouter un script à un jeu de scripts / supprimer un script d'un jeu de scripts directement sur la page d'informations sur les scripts GF

Version au 19/02/2024. Voir la dernière version.


Installer ce script?
/* eslint-disable no-multi-spaces *//* eslint-disable no-return-assign */// ==UserScript==// @name               Greasyfork script-set-edit button// @name:zh-CN         Greasyfork 快捷编辑收藏// @name:zh-TW         Greasyfork 快捷編輯收藏// @name:en            Greasyfork script-set-edit button// @name:en-US         Greasyfork script-set-edit button// @name:fr            Greasyfork Set Edit+// @namespace          Greasyfork-Favorite// @version            0.2.4.2// @description        Add / Remove script into / from script set directly in GF script info page// @description:zh-CN  在GF脚本页直接编辑收藏集// @description:zh-TW  在GF腳本頁直接編輯收藏集// @description:en     Add / Remove script into / from script set directly in GF script info page// @description:en-US  Add / Remove script into / from script set directly in GF script info page// @description:fr     Ajouter un script à un jeu de scripts / supprimer un script d'un jeu de scripts directement sur la page d'informations sur les scripts GF// @author             PY-DNG// @license            GPL-3.0-or-later// @match              http*://*.greasyfork.org/*// @match              http*://*.sleazyfork.org/*// @match              http*://greasyfork.org/*// @match              http*://sleazyfork.org/*// @require            https://update.greasyfork.org/scripts/456034/1303041/Basic%20Functions%20%28For%20userscripts%29.js// @require            https://update.greasyfork.org/scripts/449583/1324274/ConfigManager.js// @require            https://greasyfork.org/scripts/460385-gm-web-hooks/code/script.js?version=1221394// @icon               data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAAXNSR0IArs4c6QAAAbBJREFUOE+Vk7GKGlEUhr8pAiKKDlqpCDpLUCzWBxCENBa+hBsL9wHsLWxXG4tNtcGH0MIiWopY7JSGEUWsbESwUDMw4Z7siLsZDbnlPff/7n/+e67G38sA6sAXIPVWXgA/###gfinRPuhfCoXCw3Q65XA4eLBl6zvw1S2eAZqmvTqOc5/NZhkMBqRSKWzbvgYxgbwquoAX4MGyLHK5HIlEgtFo9C+IOFEAo1gsWsvlUmyPx2MymYxAhsMh6XT6lpM7BXjWdf1xNpuRz+fl8GQywTAMGo0G1WpVnJxOJ692vinADP###AaZz+cCOR6PmKZJPB4XUb/fp1wuewF+KoBCf1JVBVE5dDodms3mWdDtdqlUKl6AX+8ALmS9XgtM0/5kvNlspKX9fv8RIgBp4bISCoXo9XqsVitKpRK6rrPb7STQ7XZ7eVRaeAYerz14OBxGOfL7/eIgmUwKzHEcJZEQ1eha1wBqPxqNihufzyeQWCzmtiPPqJYM0jWIyiISibBYLAgEAtTrdVqt1nmQXN0rcH/LicqmVqvRbrdN27bfjbKru+nk7ZD3Z7q4+b++82/YPKIrXsKZ3AAAAABJRU5ErkJggg==// @grant              GM_xmlhttpRequest// @grant              GM_setValue// @grant              GM_getValue// @grant              GM_listValues// @grant              GM_deleteValue// ==/UserScript==/* global LogLevel DoLog Err $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager *//* global GMXHRHook GMDLHook ConfigManager */const GFScriptSetAPI = (function() {const API = {async getScriptSets() {const userpage = API.getUserpage();const oDom = await API.getDocument(userpage);const script_sets = Array.from($(oDom, 'ul#user-script-sets').children).map(li => {try {return {name: li.children[0].innerText,link: li.children[0].href,linkedit: li.children[1].href,id: getUrlArgv(li.children[0].href, 'set')}} catch(err) {DoLog(LogLevel.Error, [li, err, li.children.length, li.children[0]?.innerHTML, li.children[1]?.innerHTML], 'error');Err(err);}});return script_sets;},async getSetScripts(url) {return [...$All(await API.getDocument(url), '#script-set-scripts>input[name="scripts-included[]"]')].map(input => input.value);},getUserpage() {const a = $('#nav-user-info>.user-profile-link>a');return a ? a.href : null;},// editCallback recieves://     true: edit doc load success//     false: already in set// finishCallback recieves://     text: successfully added to set with text tip `text`//     true: successfully loaded document but no text tip found//     false: xhr erroraddFav(url, sid, editCallback, finishCallback) {API.modifyFav(url, oDom => {const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);if (existingInput) {editCallback(false);return false;}const input = $CrE('input');input.value = sid;input.name = 'scripts-included[]';input.type = 'hidden';$(oDom, '#script-set-scripts').appendChild(input);editCallback(true);}, oDom => {const status = $(oDom, 'p.notice');const status_text = status ? status.innerText : true;finishCallback(status_text);}, err => finishCallback(false));},// editCallback recieves://     true: edit doc load success//     false: already not in set// finishCallback recieves://     text: successfully removed from set with text tip `text`//     true: successfully loaded document but no text tip found//     false: xhr errorremoveFav(url, sid, editCallback, finishCallback) {API.modifyFav(url, oDom => {const existingInput = [...$All(oDom, '#script-set-scripts>input[name="scripts-included[]"][type="hidden"]')].find(input => input.value === sid);if (!existingInput) {editCallback(false);return false;}existingInput.remove();editCallback(true);}, oDom => {const status = $(oDom, 'p.notice');const status_text = status ? status.innerText : true;finishCallback(status_text);}, err => finishCallback(false));},async modifyFav(url, editCallback, finishCallback, onerror) {const oDom = await API.getDocument(url);if (editCallback(oDom) === false) { return false; }const form = $(oDom, '.change-script-set');const data = new FormData(form);data.append('save', '1');// Use XMLHttpRequest insteadof GM_xmlhttpRequest because there's unknown issue with GM_xmlhttpRequest// Use XMLHttpRequest insteadof GM_xmlhttpRequest before Tampermonkey 5.0.0 because of FormData posting issuesif (true || GM_info.scriptHandler === 'Tampermonkey' && !API.GM_hasVersion('5.0')) {const xhr = new XMLHttpRequest();xhr.open('POST', API.toAbsoluteURL(form.getAttribute('action')));xhr.responseType = 'blob';xhr.onload = async e => finishCallback(await API.parseDocument(xhr.response));xhr.onerror = onerror;xhr.send(data);} else {GM_xmlhttpRequest({method: 'POST',url: API.toAbsoluteURL(form.getAttribute('action')),data,responseType: 'blob',onload: async response => finishCallback(await API.parseDocument(response.response)),onerror});}},// Download and parse a url page into a html document(dom).// Returns a promise fulfills with domgetDocument(url, retry=5) {return new Promise((resolve, reject) => {GM_xmlhttpRequest({method       : 'GET',url          : url,responseType : 'blob',onload       : function(response) {if (response.status === 200) {const htmlblob = response.response;API.parseDocument(htmlblob).then(resolve).catch(reject);} else {re(response);}},onerror: err => re(err)});function re(err) {DoLog(`Get document failed, retrying: (${retry}) ${url}`);--retry > 0 ? API.getDocument(url, retry).then(resolve).catch(reject) : reject(err);}});},// Returns a promise fulfills with domparseDocument(htmlblob) {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onload = function(e) {const htmlText = reader.r###lt;const dom = new DOMParser().parseFromString(htmlText, 'text/html');resolve(dom);}reader.onerror = err => reject(err);reader.readAsText(htmlblob, document.characterSet);});},toAbsoluteURL(relativeURL, base=`${location.protocol}//${location.host}/`) {return new URL(relativeURL, base).href;},GM_hasVersion(version) {return hasVersion(GM_info?.version || '0', version);function hasVersion(ver1, ver2) {return compareVersions(ver1.toString(), ver2.toString()) >= 0;// https://greasyfork.org/app/javascript/versioncheck.js// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/manifest.json/version/formatfunction compareVersions(a, b) {if (a == b) {return 0;}let aParts = a.split('.');let bParts = b.split('.');for (let i = 0; i < aParts.length; i++) {let r###lt = compareVersionPart(aParts[i], bParts[i]);if (r###lt != 0) {return r###lt;}}// If all of a's parts are the same as b's parts, but b has additional parts, b is greater.if (bParts.length > aParts.length) {return -1;}return 0;}function compareVersionPart(partA, partB) {let partAParts = parseVersionPart(partA);let partBParts = parseVersionPart(partB);for (let i = 0; i < partAParts.length; i++) {// "A string-part that exists is always less than a string-part that doesn't exist"if (partAParts[i].length > 0 && partBParts[i].length == 0) {return -1;}if (partAParts[i].length == 0 && partBParts[i].length > 0) {return 1;}if (partAParts[i] > partBParts[i]) {return 1;}if (partAParts[i] < partBParts[i]) {return -1;}}return 0;}// It goes number, string, number, string. If it doesn't exist, then// 0 for numbers, empty string for strings.function parseVersionPart(part) {if (!part) {return [0, "", 0, ""];}let partParts = /([0-9]*)([^0-9]*)([0-9]*)([^0-9]*)/.exec(part)return [partParts[1] ? parseInt(partParts[1]) : 0,partParts[2],partParts[3] ? parseInt(partParts[3]) : 0,partParts[4]];}}}};return API;}) ();(function __MAIN__() {'use strict';const CONST = {Text: {'zh-CN': {FavEdit: '收藏集:',Add: '加入此集',Remove: '移出此集',Edit: '手动编辑',EditIframe: '页内编辑',CloseIframe: '关闭编辑',CopySID: '复制脚本ID',Sync: '同步',Working: ['工作中...', '就快好了...'],InSetStatus: ['[ ]', '[✔]'],Refreshing: {List: '获取收藏集列表...',Script: '获取收藏集内容...'},Error: {AlreadyExist: '脚本已经在此收藏集中了',NotExist: '脚本不在此收藏集中',NetworkError: '网络错误',Unknown: '未知错误'}},'zh-TW': {FavEdit: '收藏集:',Add: '加入此集',Remove: '移出此集',Edit: '手動編輯',EditIframe: '頁內編輯',CloseIframe: '關閉編輯',CopySID: '複製腳本ID',Sync: '同步',Working: ['工作中...', '就快好了...'],InSetStatus: ['[ ]', '[✔]'],Refreshing: {List: '獲取收藏集清單...',Script: '獲取收藏集內容...'},Error: {AlreadyExist: '腳本已經在此收藏集中了',NotExist: '腳本不在此收藏集中',NetworkError: '網絡錯誤',Unknown: '未知錯誤'}},'en': {FavEdit: 'Script set: ',Add: 'Add',Remove: 'Remove',Edit: 'Edit Manually',EditIframe: 'In-Page Edit',CloseIframe: 'Close Editor',CopySID: 'Copy Script-ID',Sync: 'Sync',Working: ['Working...', 'Just a moment...'],InSetStatus: ['[ ]', '[✔]'],Refreshing: {List: 'Fetching script sets...',Script: 'Fetching set content...'},Error: {AlreadyExist: 'Script is already in set',NotExist: 'Script is not in set yet',NetworkError: 'Network Error',Unknown: 'Unknown Error'}},'default': {FavEdit: 'Script set: ',Add: 'Add',Remove: 'Remove',Edit: 'Edit Manually',EditIframe: 'In-Page Edit',CloseIframe: 'Close Editor',CopySID: 'Copy Script-ID',Sync: 'Sync',Working: ['Working...', 'Just a moment...'],InSetStatus: ['[ ]', '[✔]'],Refreshing: {List: 'Fetching script sets...',Script: 'Fetching set content...'},Error: {AlreadyExist: 'Script is already in set',NotExist: 'Script is not in set yet',NetworkError: 'Network Error',Unknown: 'Unknown Error'}},},ConfigRule: {'version-key': 'config-version',ignores: [],defaultValues: {'script-sets': {'config-version': 1,},},'updaters': {/*'config-key': [function() {// This function contains updater for config['config-key'] from v0 to v1},function() {// This function contains updater for config['config-key'] from v1 to v2}]*/'script-sets': [config => {// Fill set.idconst sets = config.sets;sets.forEach(set => {const id = getUrlArgv(set.link, 'set');set.id = id;set.scripts = null; // After first refresh, it should be an array of SIDs:string});// Delete old version identifierdelete config.version;return config;}]},}}// Get i18n codelet i18n = $('#language-selector-locale') ? $('#language-selector-locale').value : navigator.language;if (!Object.keys(CONST.Text).includes(i18n)) {i18n = 'default';}const CM = new ConfigManager(CONST.ConfigRule);const CONFIG = CM.Config;CM.updateAllConfigs();loadFuncs([{name: 'Hook GM_xmlhttpRequest',checker: {type: 'switch',value: true},func: () => GMXHRHook(5)}, {name: 'Favorite panel',checker: {type: 'func',value: () => {const path = location.pathname.split('/').filter(p=>p);const index = path.indexOf('scripts');return [0,1].includes(index) && [undefined, 'code', 'feedback'].includes(path[index+2])}},func: addFavPanel}]);function addFavPanel() {if (!GFScriptSetAPI.getUserpage()) {return false;}class FavoritePanel {#CM;#sid;#sets;#elements;constructor(CM) {this.#CM = CM;this.#sid = location.pathname.match(/scripts\/(\d+)/)[1];this.#sets = this.#CM.getConfig('script-sets').sets;this.#elements = {};const script_after = $('#script-feedback-suggestion+*') || $('#new-script-discussion');const script_parent = script_after.parentElement;// Containerconst script_favorite = this.#elements.container = $$CrE({tagName: 'div',props: {id: 'script-favorite',innerHTML: CONST.Text[i18n].FavEdit},styles: { margin: '0.75em 0' }});// Selecterconst favorite_groups = this.#elements.groups = $$CrE({tagName: 'select',props: { id: 'favorite-groups' },styles: { maxWidth: '40vw' },listeners: [['change', e => {const set = this.#sets.find(set => set.id === favorite_groups.value);favorite_edit.href = set.linkedit;this.#refreshButtonDisplay();}]]});favorite_groups.id = 'favorite-groups';// Buttonsconst makeBtn = (id, innerHTML, onClick, isLink=false) => $$CrE({tagName: 'a',props: {id, innerHTML,[isLink ? 'target' : 'href']: isLink ? '_blank' : 'javascript:void(0);'},styles: { margin: '0px 0.5em' },listeners: [['click', onClick]]});const favorite_add = this.#elements.btnAdd = makeBtn('favorite-add', CONST.Text[i18n].Add, e => this.#addFav());const favorite_remove = this.#elements.btnRemove = makeBtn('favorite-remove', CONST.Text[i18n].Remove, e => this.#removeFav());const favorite_edit = this.#elements.btnEdit = makeBtn('favorite-edit', CONST.Text[i18n].Edit, e => {}, true);const favorite_iframe = this.#elements.btnIframe = makeBtn('favorite-edit-in-page', CONST.Text[i18n].EditIframe, e => this.#editInPage(e));const favorite_copy = this.#elements.btnCopy = makeBtn('favorite-add', CONST.Text[i18n].CopySID, e => copyText(this.#sid));const favorite_sync = this.#elements.btnSync = makeBtn('favorite-sync', CONST.Text[i18n].Sync, e => this.#refresh());script_favorite.appendChild(favorite_groups);script_after.before(script_favorite);[favorite_add, favorite_remove, favorite_edit, favorite_iframe, favorite_copy, favorite_sync].forEach(button => script_favorite.appendChild(button));// Text tipconst tip = this.#elements.tip = $CrE('span');script_favorite.appendChild(tip);// Display cached sets firstthis.#displaySets();// Request GF document to update setsthis.#refresh();}get sid() {return this.#sid;}get sets() {return FavoritePanel.#deepClone(this.#sets);}get elements() {return FavoritePanel.#lightClone(this.#elements);}// Request document: get sets list andasync #refresh() {this.#disable();this.#tip(CONST.Text[i18n].Refreshing.List);// Refresh sets listthis.#sets = CONFIG['script-sets'].sets = await GFScriptSetAPI.getScriptSets();this.#displaySets();// Refresh each set's script listthis.#tip(CONST.Text[i18n].Refreshing.Script);await Promise.all(this.#sets.map(async set => {// Fetch scriptsset.scripts = await GFScriptSetAPI.getSetScripts(set.linkedit);this.#displaySets();// Save to GM_storageconst setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);CONFIG['script-sets'].sets[setIndex].scripts = set.scripts;}));this.#tip();this.#enable();}#addFav() {const set = this.#getCurrentSet();const option = set.elmOption;this.#displayNotice(CONST.Text[i18n].Working[0]);GFScriptSetAPI.addFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {if (!editStatus) {this.#displayNotice(CONST.Text[i18n].Error.AlreadyExist);option.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;} else {this.#displayNotice(CONST.Text[i18n].Working[1]);}}, finishStatus => {if (finishStatus) {// Save to this.#sets and GM_storageconst setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);CONFIG['script-sets'].sets[setIndex].scripts.push(this.#sid);this.#sets = CM.getConfig('script-sets').sets;// Displaythis.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text[i18n].Error.Unknown);set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[1]} ${set.name}`;this.#displaySets();} else {this.#displayNotice(CONST.Text[i18n].Error.NetworkError);}});}#removeFav() {const set = this.#getCurrentSet();const option = set.elmOption;this.#displayNotice(CONST.Text[i18n].Working[0]);GFScriptSetAPI.removeFav(this.#getCurrentSet().linkedit, this.#sid, editStatus => {if (!editStatus) {this.#displayNotice(CONST.Text[i18n].Error.NotExist);option.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;} else {this.#displayNotice(CONST.Text[i18n].Working[1]);}}, finishStatus => {if (finishStatus) {// Save to this.#sets and GM_storageconst setIndex = CONFIG['script-sets'].sets.findIndex(s => s.id === set.id);const scriptIndex = CONFIG['script-sets'].sets[setIndex].scripts.indexOf(this.#sid);CONFIG['script-sets'].sets[setIndex].scripts.splice(scriptIndex, 1);this.#sets = CM.getConfig('script-sets').sets;// Displaythis.#displayNotice(typeof finishStatus === 'string' ? finishStatus : CONST.Text[i18n].Error.Unknown);set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[0]} ${set.name}`;this.#displaySets();} else {this.#displayNotice(CONST.Text[i18n].Error.NetworkError);}});}#editInPage(e) {e.preventDefault();const _iframes = [...$All(this.#elements.container, '.script-edit-page')];if (_iframes.length) {// Iframe exists, close iframethis.#elements.btnIframe.innerText = CONST.Text[i18n].EditIframe;_iframes.forEach(ifr => ifr.remove());this.#refresh();} else {// Iframe not exist, make iframethis.#elements.btnIframe.innerText = CONST.Text[i18n].CloseIframe;const iframe = $$CrE({tagName: 'iframe',props: {src: this.#getCurrentSet().linkedit},styles: {width: '100%',height: '60vh'},classes: ['script-edit-page'],listeners: [['load', e => {//this.#refresh();//iframe.style.height = iframe.contentDocument.body.parentElement.offsetHeight + 'px';}]]});this.#elements.container.appendChild(iframe);}}#displayNotice(text) {const notice = $CrE('p');notice.classList.add('notice');notice.id = 'fav-notice';notice.innerText = text;const old_notice = $('#fav-notice');old_notice && old_notice.parentElement.removeChild(old_notice);$('#script-content').insertAdjacentElement('afterbegin', notice);}#tip(text='', timeout=0) {this.#elements.tip.innerText = text;timeout > 0 && setTimeout(() => this.#elements.tip.innerText = '', timeout);}// Apply this.#sets to gui#displaySets() {// Save selected setconst old_value = this.#elements.groups.value;[...this.#elements.groups.children].forEach(child => child.remove());// Make <option>sthis.#sets.forEach(set => {// Create <option>set.elmOption = $$CrE({tagName: 'option',props: {innerText: set.name,value: set.id}});// Display inset statusif (set.scripts) {const inSet = set.scripts.includes(this.#sid);set.elmOption.innerText = `${CONST.Text[i18n].InSetStatus[inSet+0]} ${set.name}`;}// Append <option> into <select>this.#elements.groups.appendChild(set.elmOption);});// Adjust <select> widththis.#elements.groups.style.width = Math.max.apply(null, Array.from(this.#elements.groups.children).map(o => o.innerText.length)).toString() + 'em';// Select previous selected set's <option>const selected = old_value ? [...this.#elements.groups.children].find(option => option.value === old_value) : null;selected && (selected.selected = true);// Set edit-button.hrefconst curset = this.#sets.find(set => set.id === this.#elements.groups.value);this.#elements.btnEdit.href = curset.linkedit;// Display correct buttonthis.#refreshButtonDisplay();}// Display only add button when script in current set, otherwise remove button#refreshButtonDisplay() {const set = this.#getCurrentSet();if (!set?.scripts) { return null; }if (set.scripts.includes(this.#sid)) {this.#elements.btnAdd.style.setProperty('display', 'none');this.#elements.btnRemove.style.removeProperty('display');return true;} else {this.#elements.btnRemove.style.setProperty('display', 'none');this.#elements.btnAdd.style.removeProperty('display');return false;}}// Returns null if no <option>s yet#getCurrentSet() {return this.#sets.find(set => set.id === this.#elements.groups.value) || null;}#disable() {[this.#elements.groups,this.#elements.btnAdd, this.#elements.btnRemove,this.#elements.btnEdit, this.#elements.btnIframe,this.#elements.btnCopy, this.#elements.btnSync].forEach(element => FavoritePanel.#disableElement(element));}#enable() {[this.#elements.groups,this.#elements.btnAdd, this.#elements.btnRemove,this.#elements.btnEdit, this.#elements.btnIframe,this.#elements.btnCopy, this.#elements.btnSync].forEach(element => FavoritePanel.#enableElement(element));}static #disableElement(element) {element.style.filter = 'grayscale(1) brightness(0.95)';element.style.opacity = '0.25';element.style.pointerEvents = 'none';element.tabIndex = -1;}static #enableElement(element) {element.style.removeProperty('filter');element.style.removeProperty('opacity');element.style.removeProperty('pointer-events');element.tabIndex = 0;}static #deepClone(val) {if (typeof structuredClone === 'function') {return structuredClone(val);} else {return JSON.parse(JSON.stringify(val));}}static #lightClone(val) {if (['string', 'number', 'boolean', 'undefined', 'bigint', 'symbol', 'function'].includes(val) || val === null) {return val;}if (Array.isArray(val)) {return val.slice();}if (typeof val === 'object') {return Object.fromEntries(Object.entries(val));}}}const panel = new FavoritePanel(CM);}// Basic functions// Copy text to clipboard (needs to be called in an user event)function copyText(text) {// Create a new textarea for copyingconst newInput = document.createElement('textarea');document.body.appendChild(newInput);newInput.value = text;newInput.select();document.execCommand('copy');document.body.removeChild(newInput);}// Check whether current page url matches FuncInfo.checker rule// This code is copy and modified from FunctionLoader.checkfunction testChecker(checker) {if (!checker) {return true;}const values = Array.isArray(checker.value) ? checker.value : [checker.value]return values.some(value => {switch (checker.type) {case 'regurl': {return !!location.href.match(value);}case 'func': {try {return value();} catch (err) {DoLog(LogLevel.Error, CONST.Text.Loader.CheckerError);DoLog(LogLevel.Error, err);return false;}}case 'switch': {return value;}case 'starturl': {return location.href.startsWith(value);}case 'startpath': {return location.pathname.startsWith(value);}default: {DoLog(LogLevel.Error, CONST.Text.Loader.CheckerInvalid);return false;}}});}// Load all function-objs provided in funcs asynchronously, and merge return values into one return obj// funcobj: {[checker], [detectDom], func}function loadFuncs(oFuncs) {const returnObj = {};oFuncs.forEach(oFunc => {if (!oFunc.checker || testChecker(oFunc.checker)) {if (oFunc.detectDom) {detectDom(oFunc.detectDom, e => execute(oFunc));} else {setTimeout(e => execute(oFunc), 0);}}});return returnObj;function execute(oFunc) {setTimeout(e => {const rval = oFunc.func(returnObj) || {};copyProps(rval, returnObj);}, 0);}}function randint(min, max) {return Math.floor(Math.random() * (max - min + 1)) + min;}})();