$Config

Allows end users to configure scripts.

目前为 2022-08-13 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.gf.qytechs.cn/scripts/446506/1081060/%24Config.js

  1. // ==UserScript==
  2. // @name $Config
  3. // @author Callum Latham <callumtylerlatham@gmail.com> (https://github.com/esc-ism/tree-frame)
  4. // @exclude *
  5. // @description Allows end users to configure scripts.
  6. // ==/UserScript==
  7.  
  8. /**
  9. * A node's value.
  10. *
  11. * @typedef {boolean | number | string} NodeValue
  12. */
  13.  
  14. /**
  15. * A child node.
  16. *
  17. * @typedef {Object} ChildNode
  18. * @property {string} [label] The node's purpose.
  19. * @property {boolean | number | string} [value] The node's data.
  20. * @property {Array<NodeValue> | function(NodeValue): boolean | string} [predicate] A data validator.
  21. * @property {"color" | "date" | "datetime-local" | "email" | "month" | "password" | "search" | "tel" | "text" | "time" | "url" | "week"} [input] The desired input type.
  22. */
  23.  
  24. /**
  25. * A parent node.
  26. *
  27. * @typedef {Object} ParentNode
  28. * @property {Array<ChildNode | (ChildNode & ParentNode)>} children The node's children.
  29. * @property {ChildNode | (ChildNode & ParentNode)} [seed] - A node that may be added to children.
  30. * @property {function(Array<ChildNode>): boolean | string} [childPredicate] A child validator.
  31. * @property {function(Array<ChildNode>): boolean | string} [descendantPredicate] A descendant validator.
  32. * @property {number} [poolId] Children may be moved between nodes with poolId values that match their parent's.
  33. */
  34.  
  35. /**
  36. * A style to pass to the config-editor iFrame.
  37. *
  38. * @typedef {Object} InnerStyle
  39. * @property {number} [fontSize] The base font size for the whole frame.
  40. * @property {string} [borderTooltip] The colour of tooltip borders.
  41. * @property {string} [borderModal] The colour of the modal's border.
  42. * @property {string} [headBase] The base colour of the modal's header.
  43. * @property {'Black / White', 'Invert'} [headContrast] The method of generating a contrast colour for the modal's header.
  44. * @property {string} [headButtonExit] The colour of the modal header's exit button.
  45. * @property {string} [headButtonLabel] The colour of the modal header's exit button.
  46. * @property {string} [headButtonStyle] The colour of the modal header's style button.
  47. * @property {string} [headButtonHide] The colour of the modal header's node-hider button.
  48. * @property {string} [headButtonAlt] The colour of the modal header's alt button.
  49. * @property {Array<string>} [nodeBase] Base colours for nodes, depending on their depth.
  50. * @property {'Black / White', 'Invert'} [nodeContrast] The method of generating a contrast colour for nodes.
  51. * @property {string} [nodeButtonCreate] The colour of nodes' add-child buttons.
  52. * @property {string} [nodeButtonDuplicate] The colour of nodes' duplicate buttons.
  53. * @property {string} [nodeButtonMove] The colour of nodes' move buttons.
  54. * @property {string} [nodeButtonDisable] The colour of nodes' toggle-active buttons.
  55. * @property {string} [nodeButtonDelete] The colour of nodes' delete buttons.
  56. * @property {string} [validBackground] The colour used to show that a node's value is valid.
  57. * @property {string} [invalidBackground] The colour used to show that a node's value is invalid.
  58. */
  59.  
  60. class $Config {
  61. /**
  62. * @param {string} KEY_TREE The identifier used to store and retrieve the user's config.
  63. * @param {ParentNode} TREE_DEFAULT_RAW The tree to use as a starting point for the user's config.
  64. * @param {function(Array<ChildNode | (ChildNode & ParentNode)>): *} _getConfig Takes a root node's children and returns the data structure expected by your script.
  65. * @param {string} TITLE The heading to use in the config-editor iFrame.
  66. * @param {InnerStyle} [STYLE_INNER] A custom style to use as the default
  67. * @param {Object} [_STYLE_OUTER] CSS to assign to the frame element. e.g. {zIndex: 9999}.
  68. */
  69. constructor(KEY_TREE, TREE_DEFAULT_RAW, _getConfig, TITLE, STYLE_INNER = {}, _STYLE_OUTER = {}) {
  70. // PRIVATE STATE
  71.  
  72. let config;
  73. let isOpen = false;
  74.  
  75. // PRIVATE FUNCTIONS
  76.  
  77. const getConfig = (() => {
  78. const getStrippedForest = (children) => {
  79. const stripped = [];
  80.  
  81. for (const child of children) {
  82. if (child.isActive === false) {
  83. continue;
  84. }
  85.  
  86. const data = {};
  87.  
  88. if ('value' in child) {
  89. data.value = child.value;
  90. }
  91.  
  92. if ('label' in child) {
  93. data.label = child.label;
  94. }
  95.  
  96. if ('children' in child) {
  97. data.children = getStrippedForest(child.children);
  98. }
  99.  
  100. stripped.push(data);
  101. }
  102.  
  103. return stripped;
  104. };
  105.  
  106. return ({children}) => _getConfig(getStrippedForest(children));
  107. })();
  108.  
  109. const getError = (message, error) => {
  110. if (error) {
  111. console.error(error);
  112. }
  113.  
  114. return new Error(`[${TITLE}] ${message}`);
  115. };
  116.  
  117. // PRIVATE CONSTS
  118.  
  119. const CONFIG_DEFAULT = (() => {
  120. try {
  121. return getConfig(TREE_DEFAULT_RAW);
  122. } catch (error) {
  123. throw getError('Unable to parse default config.', error);
  124. }
  125. })();
  126.  
  127. // PUBLIC FUNCTIONS
  128.  
  129. /**
  130. * @name $Config#init
  131. * @description Instantiates the active config.
  132. * @return {Promise<void>} Resolves upon retrieving user data.
  133. */
  134. this.init = () => new Promise(async (resolve, reject) => {
  135. if (typeof GM.getValue !== 'function') {
  136. reject(getError('The GM.getValue permission is required to retrieve data.'));
  137. } else {
  138. const userTree = await GM.getValue(KEY_TREE_USER);
  139.  
  140. if (userTree) {
  141. try {
  142. config = await getConfig(userTree);
  143. } catch (error) {
  144. reject(getError('\n\nUnable to parse config.\nTry opening and closing the config editor to update your data\'s structure.', error));
  145. }
  146. } else {
  147. config = CONFIG_DEFAULT;
  148. }
  149.  
  150. resolve();
  151. }
  152. });
  153.  
  154. /**
  155. * @name $Config#reset
  156. * @description Deletes the user's data.
  157. * @return {Promise<void>} Resolves upon completing the deletion.
  158. */
  159. this.reset = () => new Promise((resolve, reject) => {
  160. if (isOpen) {
  161. reject(getError('Cannot reset while a frame is open.'));
  162. }
  163.  
  164. if (typeof GM.deleteValue !== 'function') {
  165. reject(getError('Missing GM.deleteValue permission.'));
  166. }
  167.  
  168. config = CONFIG_DEFAULT;
  169.  
  170. GM.deleteValue(KEY_TREE_USER)
  171. .then(resolve)
  172. .catch(reject);
  173. });
  174.  
  175. /**
  176. * @name $Config#get
  177. * @return {*} The active config.
  178. */
  179. this.get = () => config;
  180.  
  181. /**
  182. * @name $Config#edit
  183. * @description Allows the user to edit the active config.
  184. * @return {Promise<void>} Resolves when the user closes the config editor.
  185. */
  186. this.edit = (() => {
  187. const KEY_STYLES = 'TREE_FRAME_USER_STYLES';
  188.  
  189. const URL = {
  190. 'SCHEME': 'https',
  191. 'HOST': 'callumlatham.com',
  192. 'PATH': 'tree-frame'
  193. };
  194.  
  195. const STYLE_OUTER = Object.entries({
  196. 'position': 'fixed',
  197. 'height': '100vh',
  198. 'width': '100vw',
  199. ..._STYLE_OUTER
  200. });
  201.  
  202. // Remove functions from tree to enable postMessage transmission
  203. const [DATA_INIT, PREDICATES] = (() => {
  204. const getnumberedPredicates = (node, predicateCount) => {
  205. const predicates = [];
  206. const replacements = {};
  207.  
  208. for (const property of ['predicate', 'childPredicate', 'descendantPredicate']) {
  209. switch (typeof node[property]) {
  210. case 'number':
  211. throw getError('numbers aren\'t valid predicates');
  212.  
  213. case 'function':
  214. replacements[property] = predicateCount++;
  215.  
  216. predicates.push(node[property]);
  217. }
  218. }
  219.  
  220. if (Array.isArray(node.children)) {
  221. replacements.children = [];
  222.  
  223. for (const child of node.children) {
  224. const [replacement, childPredicates] = getnumberedPredicates(child, predicateCount);
  225.  
  226. predicateCount += childPredicates.length;
  227.  
  228. predicates.push(...childPredicates);
  229.  
  230. replacements.children.push(replacement);
  231. }
  232. }
  233.  
  234. if ('seed' in node) {
  235. const [replacement, seedPredicates] = getnumberedPredicates(node.seed, predicateCount);
  236.  
  237. predicates.push(...seedPredicates);
  238.  
  239. replacements.seed = replacement;
  240. }
  241.  
  242. return [{...node, ...replacements}, predicates];
  243. };
  244.  
  245. const [TREE_DEFAULT_PROCESSED, PREDICATES] = getnumberedPredicates(TREE_DEFAULT_RAW, 0);
  246.  
  247. return [{
  248. 'defaultTree': TREE_DEFAULT_PROCESSED,
  249. 'title': TITLE,
  250. 'defaultStyle': STYLE_INNER
  251. }, PREDICATES];
  252. })();
  253.  
  254. return new Promise(async (resolve, reject) => {
  255. if (isOpen) {
  256. reject(getError('A config editor is already open.'));
  257. } else if (typeof GM.getValue !== 'function') {
  258. reject(getError('Missing GM.getValue permission.'));
  259. } else if (typeof GM.setValue !== 'function') {
  260. reject(getError('Missing GM.setValue permission.'));
  261. } else if (typeof KEY_TREE_USER !== 'string' || KEY_TREE_USER === '') {
  262. reject(getError(`'${KEY_TREE_USER}' is not a valid storage key.`));
  263. } else {
  264. isOpen = true;
  265.  
  266. // Setup frame
  267.  
  268. const [targetWindow, frame] = (() => {
  269. const frame = document.createElement('iframe');
  270.  
  271. frame.src = `${URL.SCHEME}://${URL.HOST}/${URL.PATH}`;
  272.  
  273. for (const [property, value] of STYLE_OUTER) {
  274. frame.style[property] = value;
  275. }
  276.  
  277. let targetWindow = window;
  278.  
  279. while (targetWindow !== targetWindow.parent) {
  280. targetWindow = targetWindow.parent;
  281. }
  282.  
  283. targetWindow.document.body.appendChild(frame);
  284.  
  285. return [targetWindow, frame];
  286. })();
  287.  
  288. // Retrieve data & await frame load
  289.  
  290. const communicate = (callback = () => false) => new Promise((resolve) => {
  291. const listener = async ({origin, data}) => {
  292. if (origin === `${URL.SCHEME}://${URL.HOST}`) {
  293. let shouldContinue;
  294.  
  295. try {
  296. shouldContinue = await callback(data);
  297. } finally {
  298. if (!shouldContinue) {
  299. targetWindow.removeEventListener('message', listener);
  300.  
  301. resolve(data);
  302. }
  303. }
  304. }
  305. };
  306.  
  307. targetWindow.addEventListener('message', listener);
  308. });
  309.  
  310. const [userTree, userStyles, {'events': EVENTS, password}] = await Promise.all([
  311. GM.getValue(KEY_TREE_USER),
  312. GM.getValue(KEY_STYLES, []),
  313. communicate()
  314. ]);
  315.  
  316. // Listen for post-init communication
  317.  
  318. const sendMessage = (message) => {
  319. frame.contentWindow.postMessage(message, `${URL.SCHEME}://${URL.HOST}`);
  320. };
  321.  
  322. const closeFrame = () => new Promise((resolve) => {
  323. isOpen = false;
  324.  
  325. frame.remove();
  326.  
  327. // Wait for the DOM to update
  328. setTimeout(resolve, 1);
  329. });
  330.  
  331. communicate(async (data) => {
  332. switch (data.event) {
  333. case EVENTS.ERROR:
  334. await closeFrame();
  335.  
  336. reject(getError(
  337. '\n\nYour config\'s structure is invalid.' +
  338. '\nThis could be due to a script update or your data being corrupted.' +
  339. `\n\nError Message:\n${data.reason.replaceAll(/\n+/, '\n')}`
  340. ));
  341.  
  342. return false;
  343.  
  344. case EVENTS.PREDICATE:
  345. sendMessage({
  346. 'messageId': data.messageId,
  347. 'predicateResponse': PREDICATES[data.predicateId](
  348. Array.isArray(data.arg) ? getStrippedForest(data.arg) : data.arg
  349. )
  350. });
  351.  
  352. return true;
  353.  
  354. case EVENTS.STOP:
  355. await closeFrame();
  356.  
  357. // Save changes
  358. GM.setValue(KEY_TREE_USER, data.tree);
  359. GM.setValue(KEY_STYLES, data.styles);
  360.  
  361. config = getConfig(data.tree);
  362.  
  363. resolve();
  364.  
  365. return false;
  366.  
  367. default:
  368. reject(getError(`Message observed from tree-frame site with unrecognised 'event' value: ${data.event}`, data));
  369.  
  370. return false;
  371. }
  372. });
  373.  
  374. // Pass data
  375.  
  376. sendMessage({
  377. password,
  378. userStyles,
  379. ...(userTree ? {userTree} : {}),
  380. ...DATA_INIT
  381. });
  382. }
  383. });
  384. });
  385. };
  386. }

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址