Greasy Fork镜像 支持简体中文。

Tree Frame

A script for interfacing with my Tree Frame UI.

目前為 2022-08-08 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.gf.qytechs.cn/scripts/446506/1079242/Tree%20Frame.js

// ==UserScript==
// @name        Tree Frame
// @author      Callum Latham <[email protected]> (https://github.com/esc-ism/tree-frame)
// @exclude     *
// @description A script for interfacing with my Tree Frame UI.
// ==/UserScript==

const [getForest, editForest] = (() => {
    const USER_STYLE_STORAGE_KEY = 'TREE_FRAME_USER_STYLES';

    const URL = {
        'SCHEME': 'https',
        'HOST': 'callumlatham.com',
        'PATH': 'tree-frame'
    };

    const STYLE = {
        'position': 'fixed',
        'height': '100vh',
        'width': '100vw'
    };

    let isOpen = false;

    function 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;
    }

    return [
        (defaultTree, storageKey) => new Promise(async (resolve, reject) => {
            if (typeof GM.getValue !== 'function') {
                reject('The GM.getValue permission is required to retrieve data.');
            }

            if (storageKey) {
                GM.getValue(storageKey, defaultTree)
                    .then(({children}) => {
                        resolve(getStrippedForest(children))
                    });
            } else {
                resolve(getStrippedForest(defaultTree.children))
            }
        }),

        (storageKey, rawDefaultTree, title, defaultStyle = {}, frameStyle = {}) => new Promise(async (resolve, reject) => {
            if (isOpen) {
                reject('A Tree-Frame iFrame is already open.');
            } else if (typeof GM.getValue !== 'function' || typeof GM.setValue !== 'function') {
                reject('The tree-frame script requires GM.getValue and GM.setValue permissions.');
            } else if (typeof storageKey !== 'string' || storageKey === '') {
                reject(`'${storageKey}' is not a valid storage key.`);
            } else {
                isOpen = true;

                // Setup frame

                const [targetWindow, frame] = (() => {
                    const frame = document.createElement('iframe');

                    frame.src = `${URL.SCHEME}://${URL.HOST}/${URL.PATH}`;

                    for (const [property, value] of Object.entries({...STYLE, ...frameStyle})) {
                        frame.style[property] = value;
                    }

                    let targetWindow = window;

                    while (targetWindow !== targetWindow.parent) {
                        targetWindow = targetWindow.parent;
                    }

                    targetWindow.document.body.appendChild(frame);

                    return [targetWindow, frame];
                })();

                // Retrieve data & await frame load

                const onMessage = (callback) => {
                    const listener = async ({origin, data}) => {
                        if (origin === `${URL.SCHEME}://${URL.HOST}`) {
                            if (await callback(data)) {
                                window.removeEventListener('message', listener);
                            }
                        }
                    };

                    targetWindow.addEventListener('message', listener);
                };

                const [userTree, userStyles, {'events': EVENTS, password}] = await Promise.all([
                    GM.getValue(storageKey),
                    GM.getValue(USER_STYLE_STORAGE_KEY, []),
                    new Promise((resolve) => onMessage((data) => {
                        // Wait for a 'ready' response from the iFrame
                        resolve(data);

                        return true;
                    }))
                ]);

                // Listen for post-init communication

                const sendMessage = (message) => {
                    frame.contentWindow.postMessage(message, `${URL.SCHEME}://${URL.HOST}`);
                };

                const closeFrame = () => {
                    return new Promise(resolve => {
                        isOpen = false;

                        frame.remove();

                        // Wait for the DOM to update
                        setTimeout(resolve, 1);
                    });
                };

                // Remove functions from tree to enable postMessage transmission
                const [defaultTree, predicates] = (() => {
                    const getNumberedPredicates = (node, predicateCount) => {
                        const predicates = [];
                        const replacements = {};

                        for (const property of ['predicate', 'childPredicate', 'descendantPredicate']) {
                            switch (typeof node[property]) {
                                case 'number':
                                    throw new Error('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];
                    };

                    return getNumberedPredicates(rawDefaultTree, 0);
                })();

                onMessage(async (data) => {
                    switch (data.event) {
                        case EVENTS.ERROR:
                            await closeFrame();

                            reject(data.reason);

                            return true;

                        case EVENTS.PREDICATE:
                            sendMessage({
                                'messageId': data.messageId,
                                'predicateResponse': predicates[data.predicateId](
                                    Array.isArray(data.arg) ? getStrippedForest(data.arg) : data.arg
                                )
                            });

                            return false;

                        case EVENTS.STOP:
                            await closeFrame();

                            // Save changes
                            GM.setValue(storageKey, data.tree);
                            GM.setValue(USER_STYLE_STORAGE_KEY, data.styles);

                            resolve(getStrippedForest(data.tree.children));

                            return true;

                        default:
                            reject(`Message observed from tree-frame site with unrecognised 'event' value: ${data.event}`);

                            return false;
                    }
                });

                // Pass data

                try {
                    const config = {password, title, defaultTree, userStyles, defaultStyle};

                    if (userTree) {
                        config.userTree = userTree;
                    }

                    sendMessage(config);
                } catch ({message}) {
                    reject(message);
                }
            }
        })
    ];
})();

QingJ © 2025

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