您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Allows end users to configure scripts.
当前为
此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/446506/1401543/%24Config.js
// ==UserScript== // @name $Config // @author Callum Latham <[email protected]> (https://github.com/esc-ism/tree-frame) // @exclude * // @description Allows end users to configure scripts. // ==/UserScript== /** * A node's value. * @typedef {boolean | number | string} NodeValue */ /** * A child node. * @typedef {object} ChildNode * @property {string} [label] The node's purpose. * @property {boolean | number | string} [value] The node's data. * @property {Array<NodeValue> | function(NodeValue): boolean | string} [predicate] A data validator. * @property {"color" | "date" | "datetime-local" | "email" | "month" | "password" | "search" | "tel" | "text" | "time" | "url" | "week"} [input] The desired input type. */ /** * A parent node. * @typedef {object} ParentNode * @property {Array<ChildNode | (ChildNode & ParentNode)>} children The node's children. * @property {ChildNode | (ChildNode & ParentNode)} [seed] - A node that may be added to children. * @property {function(Array<ChildNode>): boolean | string} [childPredicate] A child validator. * @property {function(Array<ChildNode>): boolean | string} [descendantPredicate] A descendant validator. * @property {number} [poolId] Children may be moved between nodes with poolId values that match their parent's. */ /** * A style to pass to the config-editor iFrame. * @typedef {object} InnerStyle * @property {number} [fontSize] The base font size for the whole frame. * @property {string} [borderTooltip] The colour of tooltip borders. * @property {string} [borderModal] The colour of the modal's border. * @property {string} [headBase] The base colour of the modal's header. * @property {'Black / White' | 'Invert'} [headContrast] The method of generating a contrast colour for the modal's header. * @property {string} [headButtonExit] The colour of the modal header's exit button. * @property {string} [headButtonLabel] The colour of the modal header's exit button. * @property {string} [headButtonStyle] The colour of the modal header's style button. * @property {string} [headButtonHide] The colour of the modal header's node-hider button. * @property {string} [headButtonAlt] The colour of the modal header's alt button. * @property {Array<string>} [nodeBase] Base colours for nodes, depending on their depth. * @property {'Black / White' | 'Invert'} [nodeContrast] The method of generating a contrast colour for nodes. * @property {string} [nodeButtonCreate] The colour of nodes' add-child buttons. * @property {string} [nodeButtonDuplicate] The colour of nodes' duplicate buttons. * @property {string} [nodeButtonMove] The colour of nodes' move buttons. * @property {string} [nodeButtonDisable] The colour of nodes' toggle-active buttons. * @property {string} [nodeButtonDelete] The colour of nodes' delete buttons. * @property {string} [validBackground] The colour used to show that a node's value is valid. * @property {string} [invalidBackground] The colour used to show that a node's value is invalid. */ // eslint-disable-next-line no-unused-vars class $Config { /** * @param {string} KEY_TREE The identifier used to store and retrieve the user's config. * @param {ParentNode} TREE_DEFAULT_RAW The tree to use as a starting point for the user's config. * @param {function(Array<ChildNode | (ChildNode & ParentNode)>): *} _getConfig Takes a root node's children and returns the data structure expected by your script. * @param {string} TITLE The heading to use in the config-editor iFrame. * @param {InnerStyle} [STYLE_INNER] A custom style to use as the default * @param {object} [_STYLE_OUTER] CSS to assign to the frame element. e.g. {zIndex: 9999}. */ constructor(KEY_TREE, TREE_DEFAULT_RAW, _getConfig, TITLE, STYLE_INNER = {}, _STYLE_OUTER = {}) { // PRIVATE FUNCTIONS const getStrippedForest = (children) => { const stripped = []; for (const child of children) { if (child.isActive === false) { continue; } const data = {}; if ('value' in child) { data.value = child.value; } if ('label' in child) { data.label = child.label; } if ('children' in child) { data.children = getStrippedForest(child.children); } stripped.push(data); } return stripped; }; const getConfig = ({children}) => _getConfig(getStrippedForest(children)); const getError = (message, error) => { if (error) { console.error(error); } return new Error(message.includes('\n') ? `[${TITLE}]\n\n${message}` : `[${TITLE}] ${message}`); }; // PRIVATE CONSTS const URL = { SCHEME: 'https', HOST: 's3.eu-west-2.amazonaws.com', PATH: 'callumlatham.com/tree-frame-4/index.html', PARAMS: `?id=${KEY_TREE}`, }; const KEY_STYLES = 'TREE_FRAME_USER_STYLES'; const STYLE_OUTER = { position: 'fixed', height: '100vh', width: '100vw', ..._STYLE_OUTER, }; // CORE PERMISSION CHECKS if (typeof GM.getValue !== 'function') { throw getError('Missing GM.getValue permission.'); } if (typeof GM.setValue !== 'function') { throw getError('Missing GM.setValue permission.'); } if (typeof KEY_TREE !== 'string' || KEY_TREE === '') { throw getError(`'${KEY_TREE}' is not a valid storage key.`); } // PRIVATE STATE let isOpen = false; // PUBLIC FUNCTIONS const setConfig = (tree) => { const config = getConfig(tree); this.get = () => config; }; this.ready = (async () => { // Remove functions from tree to enable postMessage transmission const [DATA_INIT, PREDICATES] = (() => { const getNumberedPredicates = (node, predicateCount) => { const predicates = []; const replacements = {}; for (const property of ['predicate', 'childPredicate', 'descendantPredicate']) { switch (typeof node[property]) { case 'number': throw getError('numbers aren\'t valid predicates'); case 'function': replacements[property] = predicateCount++; predicates.push(node[property]); } } if (Array.isArray(node.children)) { replacements.children = []; for (const child of node.children) { const [replacement, childPredicates] = getNumberedPredicates(child, predicateCount); predicateCount += childPredicates.length; predicates.push(...childPredicates); replacements.children.push(replacement); } } if ('seed' in node) { const [replacement, seedPredicates] = getNumberedPredicates(node.seed, predicateCount); predicates.push(...seedPredicates); replacements.seed = replacement; } return [{...node, ...replacements}, predicates]; }; const [TREE_DEFAULT_PROCESSED, PREDICATES] = getNumberedPredicates(TREE_DEFAULT_RAW, 0); return [ { defaultTree: TREE_DEFAULT_PROCESSED, title: TITLE, defaultStyle: STYLE_INNER, }, PREDICATES, ]; })(); // Setup frame const [targetWindow, frame] = (() => { const frame = document.createElement('iframe'); frame.src = `${URL.SCHEME}://${URL.HOST}/${URL.PATH}${URL.PARAMS}`; for (const [property, value] of Object.entries(STYLE_OUTER)) { frame.style[property] = value; } frame.style.display = 'none'; let targetWindow = window; while (targetWindow !== targetWindow.parent) { targetWindow = targetWindow.parent; } targetWindow.document.body.appendChild(frame); return [targetWindow, frame]; })(); // Retrieve data & await frame load const communicate = async (callback = () => false) => { const getShouldContinue = async ({origin, data}) => { if (origin === `${URL.SCHEME}://${URL.HOST}` && data.id === KEY_TREE) { return await callback(data); } return true; }; return await new Promise((resolve, reject) => { const listener = (message) => getShouldContinue(message) .then((shouldContinue) => { if (!shouldContinue) { resolve(message.data); targetWindow.removeEventListener('message', listener); } }) .catch((error) => { targetWindow.removeEventListener('message', listener); reject(getError(error.message, error)); }); targetWindow.addEventListener('message', listener); }); }; const [userTree, userStyles, {'events': EVENTS, password}] = await Promise.all([ GM.getValue(KEY_TREE), GM.getValue(KEY_STYLES, []), communicate(), ]); // Listen for post-init communication const sendMessage = (message) => { frame.contentWindow.postMessage({...message, id: KEY_TREE}, `${URL.SCHEME}://${URL.HOST}`); }; const openFrame = (doOpen = true) => new Promise((resolve) => { isOpen = doOpen; frame.style.display = doOpen ? (STYLE_OUTER.display ?? 'initial') : 'none'; // Delay script execution until visual update setTimeout(resolve, 0); }); const disconnectFrame = () => new Promise((resolve) => { isOpen = false; frame.remove(); // Delay script execution until visual update setTimeout(resolve, 0); }); /** * @name $Config#reset * @description Deletes the user's data. * @returns {Promise<void>} Resolves upon completing the deletion. */ this.reset = async () => { if (isOpen) { throw getError('Cannot reset while a frame is open.'); } if (typeof GM.deleteValue !== 'function') { throw getError('Missing GM.deleteValue permission.'); } try { setConfig(TREE_DEFAULT_RAW); } catch (error) { throw getError('Unable to parse default config.', error); } sendMessage({ password, event: EVENTS.RESET, }); await GM.deleteValue(KEY_TREE); this.ready = Promise.resolve(); }; const sendPredicateResponse = (data) => sendMessage({ password, event: EVENTS.PREDICATE, messageId: data.messageId, predicateResponse: PREDICATES[data.predicateId]( Array.isArray(data.arg) ? getStrippedForest(data.arg) : data.arg, ), }); /** * @name $Config#edit * @description Allows the user to edit the active config. * @returns {Promise<void>} Resolves when the user closes the config editor. */ this.edit = async () => { if (isOpen) { throw getError('A config editor is already open.'); } openFrame(); await communicate(async (data) => { switch (data.event) { case EVENTS.STOP: // Save changes GM.setValue(KEY_TREE, data.tree); GM.setValue(KEY_STYLES, data.styles); setConfig(data.tree); await openFrame(false); return false; case EVENTS.PREDICATE: sendPredicateResponse(data); return true; } console.warn(getError(`Message observed from tree-frame site with unrecognised 'event' value: ${data.event}`, data)); return true; }); }; // Pass data sendMessage({ password, event: EVENTS.START, userStyles, ...(userTree ? {userTree} : {}), ...DATA_INIT, }); await communicate(async (data) => { switch (data.event) { case EVENTS.PREDICATE: sendPredicateResponse(data); return true; case EVENTS.ERROR: // Flags that removing userTree won't help delete this.reset; await disconnectFrame(); throw getError( 'Your config is invalid.' + '\nThis could be due to a script update or your data being corrupted.' + `\n\nError Message:\n${data.reason.replaceAll(/\n+/g, '\n')}`, ); case EVENTS.RESET: throw getError( 'Your config is invalid.' + '\nThis could be due to a script update or your data being corrupted.' + `\n\nError Message:\n${data.reason.replaceAll(/\n+/g, '\n')}`, ); case EVENTS.START: setConfig(data.tree); return false; } console.warn(getError(`Message observed from tree-frame site with unrecognised 'event' value: ${data.event}`, data)); return true; }); })(); } }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址