Greasy Fork is available in English.

R4 Settings

R4 Settings Library

Script này sẽ không được không được cài đặt trực tiếp. Nó là một thư viện cho các script khác để bao gồm các chỉ thị meta // @require https://update.greasyfork.org/scripts/482052/1302000/R4%20Settings.js

// ==UserScript==
// @name            R4 Settings
// @description     R4 Settings Library
// @version         1.4.8
// @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.addStyle === undefined) {
        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,
    }
}