Greasy Fork

R4 Settings

R4 Settings Library

目前为 2023-12-19 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.cloud/scripts/482052/1298681/R4%20Settings.js

function R4Settings(options = {}) {

    const images = R4Images();

    GM.addStyle(`
    /* css */

    /* Settings */

    .r4-settings {
        position: relative;
    }

    .r4-settings > ul {
        width: 350px;
        display: none;
        background: #313131;
        border-top: 0;
        position: absolute;
        top: 50px;
        left: 0px;
        white-space: nowrap;
        box-shadow: 0 5px 20px 0px #000;
        border-color: #222d33;
        border-style: solid;
        border-width: 3px 3px 3px 3px;
        padding: 5px 0 0 0;
    }
    .r4-settings > ul:before {
        content: '';
        display: block;
        position: absolute;
        top: -13px;
        left: 20px;
        width: 0;
        height: 0;
        border-left: 10px solid transparent;
        border-right: 10px solid transparent;
        border-bottom: 10px solid #222d33;
    }

    .r4-settings > ul:after {
        content: '';
        display: block;
        position: absolute;
        top: -9px;
        left: 21px;
        width: 0;
        height: 0;
        border-left: 9px solid transparent;
        border-right: 9px solid transparent;
        border-bottom: 9px solid #313131;
    }

    body.r4-settings-active .r4-settings > ul {
        display: block !important;
    }

    .r4-settings > ul > li,
    .r4-setting-submenu > ul > li {
        color: #777;
        font-size: 10px;
        font-weight: bold;
        margin: 0 !important;
        padding-left: 10px;
        padding-right: 10px;
        padding-top: 5px;
        padding-bottom: 5px;
        min-height: 30px;
    }


    .r4-settings > ul > li .r4-setting,
    .r4-setting-submenu > ul > li .r4-setting {
        display: inline-block;
        width: 100%;
    }

    .r4-settings > ul > li .r4-tumbler,
    .r4-setting-submenu > ul > li .r4-tumbler {
        float: right;
    }

    .r4-settings .r4-setting-header {
        text-align: center;
    }

    .r4-settings .r4-setting-text-value {
        display: block;
        opacity: .5;
    }

    .r4-settings .r4-setting-text-block {
        float: left;
        position: relative;
        padding-top: 5px;
    }

    .r4-setting-submenu {
        position: relative;
        cursor: pointer;
    }

    .r4-setting-submenu > ul {
        background: #212121;
        margin: 30px -10px 0;
        padding: 10px 0;
        cursor: auto;
    }

    .r4-settings > ul > li:last-child .r4-setting-submenu > ul {
        margin-bottom: -5px;
    }

    .r4-setting-submenu-arrow {
        float: right;
        width: 15px;
        height: 15px;
        margin-right: 10px;
        margin-top: 5px;
        background-size: 15px 15px;
        background-repeat: no-repeat;
        background-image: url(${images.arrow});
        filter: invert(100%) sepia(95%) saturate(21%) hue-rotate(280deg) brightness(106%) contrast(106%);
        transform: rotate(180deg);
    }

    /* Tumbler */

    .r4-tumbler {
        width: 38px;
        height: 30px;
        background-color: #000;
        border: #1d92b2;
        border-radius: 30px;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding: 0 6px;
        cursor: pointer;
        position: relative;
        user-select: none;
        box-sizing: content-box;
    }
    .r4-tumbler-point {
        border-radius: 50%;
        content: '';
        display: block;
        height: 20px;
        width: 20px;
        background-color: #999;
        background-clip: content-box;
        box-sizing: border-box;
        border-color: transparent;
        border-style: solid;
        border-width: 5px;
    }
    .r4-tumbler > .r4-tumbler-dot {
        position: absolute;
        height: 20px;
        width: 20px;
        border-radius: 50%;
        background-color: #fff;
        transition: transform .5s,background-color .5s;
        will-change: transform;
    }

    /* Tumbler On-Off */

    .r4-on-of-tumbler .r4-tumbler-point:nth-child(1) {
        background-color: green;
    }
    .r4-on-of-tumbler .r4-tumbler-point:nth-child(2) {
        background-color: indianred;
    }

    /* Tumbler Settings */

    .r4-tumbler-settings {
        width: 40px !important;
    }
    .r4-tumbler-settings .r4-tumbler-point {
        background-size: 15px 15px;
        background-repeat: no-repeat;
        background-position: center;
        border-width: 2px;
    }
    .r4-tumbler-settings .r4-tumbler-point:nth-child(1) {
        background-image: url('${images.settings}');
        background-color: transparent !important;
    }
    .r4-tumbler-settings .r4-tumbler-point:nth-child(2) {
        background-image: url('${images.settingsclose}');
        background-color: transparent !important;
    }

    .r4-tumbler-settings-update,
    .r4-tumbler-settings-update:hover {
        height: 30px;
        background: #f4363630;
        position: absolute;
        left: 0;
        margin-left: 30px;
        margin-top: 5px;
        border-radius: 30px;
        color: #b44b44 !important;
        line-height: 30px;
        padding: 0 20px 0 40px;
        cursor: pointer;
        text-decoration: none;
    }

    /* Tooltip */

    .r4-tooltip {
        position: relative;
        display: inline-block;
    }

    .r4-tooltip .tooltiptext {
        background: #313131;
        border-top: 0;
        position: absolute;
        top: -10px;
        left: 35px;
        white-space: nowrap;
        box-shadow: 0 5px 20px 0px #000;
        border-color: #222d33;
        border-style: solid;
        border-width: 3px;
        visibility: hidden;
        width: 300px;
        white-space: normal;
        padding: 15px;
        position: absolute;
        z-index: 3;
    }

    .r4-tooltip:hover .tooltiptext {
        visibility: visible;
    }

    .r4-tooltip .tooltiptext:before {
        content: '';
        display: block;
        position: absolute;
        left: -13px;
        top: 11px;
        width: 0;
        height: 0;
        border-top: 10px solid transparent;
        border-bottom: 10px solid transparent;
        border-right: 10px solid #222d33;
    }

    .r4-tooltip .tooltiptext:after {
        content: '';
        display: block;
        position: absolute;
        left: -9px;
        top: 12px;
        width: 0;
        height: 0;
        border-top: 9px solid transparent;
        border-bottom: 9px solid transparent;
        border-right: 9px solid #222d33;
    }

    .r4-tooltip-icon {
        border-radius: 50%;
        background: #777;
        width: 14px;
        height: 14px;
        display: inline-block;
        text-align: center;
        color: #000;
        text-transform: lowercase;
        cursor: pointer;
        font-family: monospace, monospace;
        font-size: 13px;
        margin: 8px;
    }

    /* !css */
    `);

    var tumbler;

    buildSettings();

    async function setSetting(name, value) {
        await GM.setValue(name, value);
        console.debug(`Saved setting ${name}: ${JSON.stringify(value)}`);
    }

    async function deleteSetting(name) {
        await GM.deleteValue(name);
    }

    async function getSetting(name) {
        let value = await GM.getValue(name);
        if (value !== undefined) {
            console.debug(`Got setting ${name}: ${JSON.stringify(value)}`);
        } else {
            value = options.missingSettingHandler?.(name);
        }
        return value;
    }

    async function setCongigSetting(config, option) {
        if (option.value !== undefined) {
            await setSetting(config.name, option.value);
        } else {
            await deleteSetting(config.name);
        }
    }

    async function getConfigSetting(config) {
        return await getSetting(config.name);
    }

    async function getCurrentOption(config) {
        const currentSetting = await getConfigSetting(config);

        for (const tumblerOption of config.options) {
            const optionSetting = tumblerOption.value;
            if (optionSetting === currentSetting) {
                return tumblerOption;
            }
        }

        const option = getDefaultOption(config);
        await setCongigSetting(config, option);
        return option;
    }

    async function rotateSetting(config) {
        const currentOption = await getCurrentOption(config);
        const nextOption = getNextOption(config, currentOption);
        await setCongigSetting(config, nextOption);
        setBodyClass(config, nextOption);
        if (nextOption.reload === true) {
            document.location.reload();
        }
        if (nextOption.start) {
            nextOption.start();
        }
        if (nextOption.end) {
            nextOption.end();
        }
    }

    function getDefaultOption(config) {
        for (const tumblerOption of config.options) {
            if (tumblerOption.default === true) {
                return tumblerOption;
            }
        }
        return config.options[0];
    }

    function setBodyClass(config, option) {
        for (const tumblerOption of config.options) {
            if (tumblerOption.class) {
                document.body.classList.remove(tumblerOption.class);
            }
        }

        if (option?.class) {
            document.body.classList.add(option.class);
        }
    }

    function getNextOption(config, option) {
        let nextOptionIndex;
        if (option) {
            const currentOptionIndex = config.options.indexOf(option);
            if (currentOptionIndex < config.options.length - 1) {
                nextOptionIndex = currentOptionIndex + 1;
            } else {
                nextOptionIndex = 0;
            }
        } else {
            nextOptionIndex = 1;
        }
        return config.options[nextOptionIndex];
    }

    const state = {
        events: {
            start: {
                fired: false,
            },
            end: {
                fired: false,
            },
        }
    }

    function afterStart(callback) {
        if (state.events.start.fired === true) {
            callback();
        } else {
            document.addEventListener("R4SettingsStart", callback);
        }
    }

    function afterEnd(callback) {
        if (state.events.end.fired === true) {
            callback();
        } else {
            document.addEventListener("R4SettingsEnd", callback);
        }
    }

    async function initSetting(config) {
        const currentOption = await getCurrentOption(config);
        afterStart(() => {
            setBodyClass(config, currentOption);
        });
        if (config?.start) {
            afterStart(() => {
                config.start();
            });
        }
        if (currentOption?.start) {
            afterStart(() => {
                currentOption.start();
            });
        }
        if (config?.end) {
            afterEnd(() => {
                config.end();
            });
        }
        if (currentOption?.end) {
            afterEnd(() => {
                currentOption.end();
            });
        }
    }

    function buildSettings() {
        tumbler = buildTumbler({
            handler: toggle,
            name: "settings",
            classes: [],
            options: [
                {
                    class: null,
                },
                {
                    class: "r4-settings-active",
                },
            ],
        });
        tumbler.classList.add("r4-settings");
        tumbler.classList.add("pull-right");

        const dropdown = document.createElement("ul");
        tumbler.appendChild(dropdown);

        const item = document.createElement("div");
        item.classList.add("r4-setting-header");

        if (options.script_homepage) {
            const name = document.createElement("div");
            name.classList.add("r4-setting-label");
            name.innerHTML = `<a href="${options.script_homepage}" target="_blank">${GM.info.script.name}</a>`;
            item.appendChild(name);

            const feedback = document.createElement("div");
            feedback.classList.add("r4-setting-text-value");
            feedback.innerHTML = `<a href="${options.script_homepage}/feedback" target="_blank">${options.feedback_text || "Feedback"}</a>`;
            item.appendChild(feedback);

            GM.xmlhttpRequest({
                method: "GET",
                url: options.script_homepage,
                onload(response) {
                    console.debug(response);
                    if (response.status === 200) {
                        const patern =
                            /<a class="install-link" [^>]* data-script-version="(?<version>[^"]*)" [^>]* href="(?<href>[^"]*)"[^>]*>/;
                        const results = patern.exec(response.responseText);
                        if (results?.groups) {
                            if (results.groups.version != GM.info.script.version) {
                                const updateURL = results.groups.href;
                                const updateTumbler = document.createElement("a");
                                updateTumbler.href = updateURL;
                                updateTumbler.classList.add("r4-tumbler-settings-update");
                                updateTumbler.innerText = options.update_text || "Update";

                                tumbler.insertBefore(updateTumbler, tumbler.firstChild);
                            }
                        } else {
                            console.debug(`Failed to parse install link`);
                        }
                    }
                },
                onerror(e) {
                    console.debug(`Failed to request install link`);
                    console.debug(e);
                },
            });

        } else {

            const name = document.createElement("div");
            name.classList.add("r4-setting-label");
            name.innerHTML = GM.info.script.name;
            item.appendChild(name);

        }

        const version = document.createElement("div");
        version.classList.add("r4-setting-text-value");
        version.innerHTML = `${options.version_text || "Version"}: ${GM.info.script.version}`;
        item.appendChild(version);

        addElementSetting(item);

        document.addEventListener("click", close);
    }

    function toggle(event) {
        document.body.classList.toggle("r4-settings-active");
        event.stopPropagation();
        event.preventDefault();
    }

    function close(event) {
        if (!event.target.closest(".r4-settings")) {
            document.body.classList.remove("r4-settings-active");
        }
    }

    function findSubmenu(config) {
        const submenuAll = tumbler.querySelectorAll(".r4-setting-submenu");
        const submenuFiltered = Array.from(submenuAll).find(
            (el) => el.querySelector(".r4-setting-label").textContent === config.submenu
        );
        if (submenuFiltered) {
            return submenuFiltered.querySelector("ul");
        }
    }

    function createSubmenu(config) {
        const settingTextBlock = buildSettingTextBlock(config.submenu);
        const submenu = document.createElement("ul");
        submenu.addEventListener("click", (event) => {
            event.stopPropagation();
        });
        submenu.classList.add("hidden");
        const submenuArrow = document.createElement("span");
        submenuArrow.classList.add("r4-setting-submenu-arrow");
        const submenuElem = document.createElement("div");
        submenuElem.classList.add("r4-setting");
        submenuElem.classList.add("r4-setting-submenu");
        submenuElem.appendChild(settingTextBlock);
        submenuElem.appendChild(submenuArrow);
        submenuElem.appendChild(submenu);
        const submenuItem = document.createElement("li");
        submenuItem.appendChild(submenuElem);
        submenuItem.addEventListener("click", (event) => {
            submenu.classList.toggle("hidden");
        });
        const dropdown = tumbler.querySelector("ul");
        dropdown.appendChild(submenuItem);
        return submenu;
    }

    function addElementSetting(element, config) {
        let container;

        if (config?.submenu) {
            let submenu = findSubmenu(config);
            if (!submenu) {
                submenu = createSubmenu(config);
            }
            container = submenu;
        } else {
            const dropdown = tumbler.querySelector("ul");
            container = dropdown;
        }

        const item = document.createElement("li");
        item.appendChild(element);
        container.appendChild(item);
    }

    function buildTumbler(config) {
        const optionsLength = config.options.length;
        const tumblerClassName = `r4-tumbler-${config.name}`;

        GM.addStyle(`
        /* css */

        .${tumblerClassName} {
            width: ${optionsLength * 15 + optionsLength * 5}px !important;
        }

        /* !css */
        `);

        const tumblerWrapper = document.createElement("div");
        tumblerWrapper.classList.add("r4-tumbler-wrapper");

        const tumbler = document.createElement("div");
        tumbler.classList.add("r4-tumbler");
        tumbler.classList.add(tumblerClassName);
        tumbler.className += ` ${config.classes.join(" ")}`;

        tumbler.addEventListener("click", config.handler);

        for (let optionIndex = 0; optionIndex < optionsLength; optionIndex++) {
            const tumblerOption = config.options[optionIndex];
            const tumblerPoint = document.createElement("div");
            tumblerPoint.classList.add("r4-tumbler-point");
            tumbler.appendChild(tumblerPoint);

            if (tumblerOption.class !== null) {
                // Add dot move style for all points except initial
                const enabledClassName = tumblerOption.class;

                GM.addStyle(`
                /* css */

                .${enabledClassName} .${tumblerClassName} .r4-tumbler-dot {
                    transform: translateX(${optionIndex * 100}%);
                }

                /* !css */
                `);
            }
        }

        const tumblerDot = document.createElement("div");
        tumblerDot.classList.add("r4-tumbler-dot");
        tumbler.appendChild(tumblerDot);

        tumblerWrapper.appendChild(tumbler);

        return tumblerWrapper;
    }

    function buildSettingTextBlock(label) {
        const settingTextBlock = document.createElement("div");
        settingTextBlock.classList.add("r4-setting-text-block");

        const labelSpan = document.createElement("span");
        labelSpan.classList.add("r4-setting-label");
        labelSpan.innerHTML = label;

        settingTextBlock.appendChild(labelSpan);

        return settingTextBlock;
    }

    function buildTumblerSetting(config) {
        for (const tumplerOption of config.options) {
            if (tumplerOption.class === undefined && tumplerOption.value !== undefined && tumplerOption.value !== null) {
                tumplerOption.class = config.name + "-" + tumplerOption.value;
            }
            if (tumplerOption.value === undefined && tumplerOption.class !== undefined) {
                tumplerOption.value = tumplerOption.class;
            }
        }

        initSetting(config);
        const originalHandler = config.handler;
        config.handler = async (event) => {
            await rotateSetting(config);
            originalHandler?.(event);
        };

        const tumblerWrapper = buildTumbler(config);
        tumblerWrapper.classList.add("r4-setting");

        const settingClass = `r4-setting-${config.name}`;
        tumblerWrapper.classList.add(settingClass);

        const settingTextBlock = buildSettingTextBlock(config.label);

        const defaultSelectors = [];
        for (const tumplerOption of config.options) {
            if (tumplerOption.class !== null) {
                defaultSelectors.push(`body.${tumplerOption.class} .${settingClass} .r4-setting-text-value-1`);
            }
        }

        for (const tumplerOption of config.options) {
            const optionIndex = config.options.indexOf(tumplerOption);
            const textValueClass = `r4-setting-text-value-${optionIndex + 1}`;

            const textValueSpan = document.createElement("span");
            textValueSpan.classList.add("r4-setting-text-value");
            textValueSpan.classList.add(textValueClass);
            textValueSpan.innerHTML = tumplerOption.text;
            settingTextBlock.appendChild(textValueSpan);

            if (optionIndex == 0) {
                GM.addStyle(`
                /* css */

                ${defaultSelectors.join(",")} {
                    display: none !important;
                }

                /* !css */
                `);
            } else {
                const enabledClassName = tumplerOption.class;
                GM.addStyle(`
                /* css */

                body:not(.${enabledClassName}) .${settingClass} .${textValueClass} {
                    display: none !important;
                }

                /* !css */
                `);
            }
        }

        tumblerWrapper.appendChild(settingTextBlock);

        return tumblerWrapper;
    }

    function createTumblerSetting(config, wrapSetting = tumblerSetting => tumblerSetting) {
        const tumblerSetting = buildTumblerSetting(config);
        addElementSetting(wrapSetting(tumblerSetting), config);
    }

    if (document.body) {
        state.events.start.fired = true;
    } else {
        new MutationObserver((mutationList, observer) => {
            if (document.body && !state.events.start.fired) {
                document.dispatchEvent(new Event("R4SettingsStart"));
                state.events.start.fired = true;
                observer.disconnect();
            }
        }).observe(document.documentElement, {childList: true});
    }

    if (/complete|interactive|loaded/.test(document.readyState)) {
        state.events.end.fired = true;
    } else {
        document.addEventListener("DOMContentLoaded", () => {
            document.dispatchEvent(new Event("R4SettingsEnd"));
            state.events.end.fired = true;
        });
    }

    return {
        tumbler,
        buildTumblerSetting,
        createTumblerSetting,
        addElementSetting,
        setSetting,
        getSetting,
        afterStart,
        afterEnd,
    }
}