R4 Settings

R4 Settings Library

此脚本不应直接安装。它是供其他脚本使用的外部库,要使用该库请加入元指令 // @require https://update.gf.qytechs.cn/scripts/482052/1502912/R4%20Settings.js

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name            R4 Settings
// @description     R4 Settings Library
// @version         1.4.9
// @grant           GM.info
// @grant           GM.addStyle
// @grant           GM.xmlHttpRequest
// @grant           GM.setValue
// @grant           GM.getValue
// @grant           GM.deleteValue
// @require         https://update.greasyfork.org/scripts/482042/1298685/R4%20Images.js
// @require         https://update.greasyfork.org/scripts/482597/1301960/R4%20Utils.js
// ==/UserScript==

    /* ------------------------------------------------- */
    /* --------------Polyfills-------------------------- */
    /* ------------------------------------------------- */


    // Polyfill for Greasemonkey extension

    function R4_addStyle (aCss) {
        let head = document.getElementsByTagName('head')[0];
        if (head) {
            let style = document.createElement('style');
            style.setAttribute('type', 'text/css');
            style.textContent = aCss;
            head.appendChild(style);
            return style;
        }
        return null;
    };
 
    if (GM.info?.scriptHandler === 'Userscripts') {
        console.log("Overriding GM.addStyle for Safari Userscripts extension.");
        GM.addStyle = R4_addStyle;
    }
    
    // Polyfill for Safari Userscripts extension

    const original_GM_xmlHttpRequest = GM.xmlHttpRequest;

    async function R4_xmlHttpRequest(details) {

        if (details.onload) {
            const original_onload = details.onload;
            details.onload = (response) => {
                if (response.responseURL) {
                    response.finalUrl = response.responseURL;
                }
                return original_onload(response);
            }
        }

        return await original_GM_xmlHttpRequest(details);
    }

    GM.xmlHttpRequest = R4_xmlHttpRequest;


/* ------------------------------------------------- */
/* --------------Settings--------------------------- */
/* ------------------------------------------------- */

function R4Settings(options = {}) {

    const utils = R4Utils();
    const images = R4Images();

    GM.addStyle(`
    /* css */

    /* Settings */

    .r4-settings {
        position: relative;
    }

    .r4-settings > ul {
        width: 350px;
        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;
    }

    .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 */
    `);

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

    const elements = {
        tumbler: null,
        dropdown: null,
    };

    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) {
        const value = await GM.getValue(name);
        if (value === undefined) {
            return options.missingSettingHandler?.(name);
        }
        console.debug(`Got setting ${name}: ${JSON.stringify(value)}`);
        return value;
    }

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

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

    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() {
        elements.tumbler = buildTumbler({
            handler: toggle,
            name: "settings",
            classes: ["r4-settings", "pull-right"],
            options: [
                {
                    class: null,
                },
                {
                    class: "r4-settings-active",
                },
            ],
        });

        elements.dropdown = utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <ul class="hidden"></ul>
            <!-- !html -->
            `
        );

        elements.tumbler.appendChild(elements.dropdown);

        const header = utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <div class="r4-setting-header"></div>
            <!-- !html -->
            `
        );

        if (options.script_homepage) {
            
            header.appendChild(utils.fromHTML(
                /* html */
                `
                <!-- html -->
                <div class="r4-setting-label">
                    <a href="${options.script_homepage}" target="_blank">
                        ${GM.info.script.name}
                    </a>
                </div>
                <!-- !html -->
                `
            ));
            
            header.appendChild(utils.fromHTML(
                /* html */
                `
                <!-- html -->
                <div class="r4-setting-text-value">
                    <a href="${options.script_homepage}/feedback" target="_blank">
                        ${options.feedback_text || "Feedback"}
                    </a>
                </div>
                <!-- !html -->
                `
            ));

            GM.xmlHttpRequest({
                method: "GET",
                url: options.script_homepage,
                onload(response) {
                    console.debug(`Response ${response.status} for ${response.finalUrl}`, {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) {
                            console.debug(`Failed to parse install link`);
                            return;
                        }
                        if (results.groups.version == GM.info.script.version) {
                            return;
                        }
                        console.log(`New version ${results.groups.version} is available`);

                        elements.tumbler.insertBefore(utils.fromHTML(
                            /* html */
                            `
                            <!-- html -->
                            <a class="r4-tumbler-settings-update" href="${results.groups.href}" target="_blank">
                                ${options.update_text || "Update"}
                            </a>
                            <!-- !html -->
                            `
                        ), elements.tumbler.firstChild);
                    }
                },
                onerror(e) {
                    console.debug(`Failed to request install link`);
                    console.debug("Error:", {e});
                },
            });

        } else {
            
            header.appendChild(utils.fromHTML(
                /* html */
                `
                <!-- html -->
                <div class="r4-setting-label">
                    ${GM.info.script.name}
                </div>
                <!-- !html -->
                `
            ));

        }
            
        header.appendChild(utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <div class="r4-setting-text-value">
                ${options.version_text || "Version"}: ${GM.info.script.version}
            </div>
            <!-- !html -->
            `
        ));

        addElementSetting(header);

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

    function toggle(event) {
        elements.dropdown.classList.toggle("hidden");
        event.stopPropagation();
        event.preventDefault();
    }

    function close(event) {
        if (!event.target.closest(".r4-settings")) {
            elements.dropdown.classList.add("hidden");
        }
    }

    function findSubmenu(config) {
        const submenuAll = elements.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 submenuItem = utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <li>
                <div class="r4-setting r4-setting-submenu">
                    <span class="r4-setting-submenu-arrow"></span>
                    <ul class="hidden"></ul>
                </div>
            </li>
            <!-- !html -->
            `
        )

        const submenuElem = submenuItem.querySelector(".r4-setting-submenu");
        submenuElem.insertBefore(
            buildSettingTextBlock(config.submenu), 
            submenuElem.firstChild
        );

        const submenu = submenuElem.querySelector("ul");
        submenu.addEventListener("click", (event) => {
            event.stopPropagation();
        });

        submenuItem.addEventListener("click", (event) => {
            submenu.classList.toggle("hidden");
        });

        elements.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 = elements.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 = utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <div class="r4-tumbler-wrapper ${config.classes.join(" ")}">
                <div class="r4-tumbler ${tumblerClassName}"></div>
            </div>
            <!-- !html -->
            `
        );

        const tumbler = tumblerWrapper.querySelector(".r4-tumbler");
        tumbler.addEventListener("click", config.handler);

        for (let optionIndex = 0; optionIndex < optionsLength; optionIndex++) {
            const tumblerOption = config.options[optionIndex];
            const tumblerPoint = utils.fromHTML(
                /* html */
                `
                <!-- html -->
                <div class="r4-tumbler-point"></div>
                <!-- !html -->
                `
            );
            tumbler.appendChild(tumblerPoint);

            if (tumblerOption.class) {
                GM.addStyle(`
                /* css */

                .${tumblerOption.class} .${tumblerClassName} .r4-tumbler-dot {
                    transform: translateX(${optionIndex * 100}%) !important;
                }

                /* !css */
                `);
            } else {
                GM.addStyle(`
                /* css */

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

                /* !css */
                `);
            }
        }

        tumbler.appendChild(utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <div class="r4-tumbler-dot"></div>
            <!-- !html -->
            `
        ));

        return tumblerWrapper;
    }

    function buildSettingTextBlock(label) {
        return utils.fromHTML(
            /* html */
            `
            <!-- html -->
            <div class="r4-setting-text-block">
                <span class="r4-setting-label">${label}</span>
            </div>
            <!-- !html -->
            `
        );
    }

    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);

        let textValueClassEmpty = null;
        for (const tumblerOption of config.options) {
            if (!tumblerOption.class) {
                const optionIndex = config.options.indexOf(tumblerOption);
                textValueClassEmpty = `r4-setting-text-value-${optionIndex + 1}`;
            }
        }

        const emptySelectors = [];
        for (const tumblerOption of config.options) {
            if (tumblerOption.class) {
                emptySelectors.push(`body.${tumblerOption.class} .${settingClass} .${textValueClassEmpty}`);
            }
        }

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

            settingTextBlock.appendChild(utils.fromHTML(
                /* html */
                `
                <!-- html -->
                <span class="r4-setting-text-value ${textValueClass}">
                    ${tumblerOption.text}
                </span>
                <!-- !html -->
                `
            ));

            if (tumblerOption.class) {
                GM.addStyle(`
                /* css */

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

                /* !css */
                `);
            } else {
                GM.addStyle(`
                /* css */

                ${emptySelectors.join(",")} {
                    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: elements.tumbler,
        buildTumblerSetting,
        createTumblerSetting,
        addElementSetting,
        setSetting,
        getSetting,
        afterStart,
        afterEnd,
    }
}