R4 Settings

R4 Settings Library

אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyfork.org/scripts/482052/1502912/R4%20Settings.js

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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,
    }
}