Masiro: Blurs NSFW Covers

Blurs the covers of NSFW novels on Masiro.

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               Masiro: Blurs NSFW Covers
// @name:zh-TW         真白萌:模糊 R17 封面
// @name:zh-CN         真白萌:模糊 R17 封面
// @description        Blurs the covers of NSFW novels on Masiro.
// @description:zh-TW  模糊真白萌 R17 小說的封面。
// @description:zh-CN  模糊真白萌 R17 小说的封面。
// @icon               https://icons.duckduckgo.com/ip3/masiro.me.ico
// @author             Jason Kwok
// @namespace          https://jasonhk.dev/
// @version            1.4.0
// @license            MIT
// @match              https://masiro.me/admin
// @match              https://masiro.me/admin/
// @match              https://masiro.me/admin/novels
// @match              https://masiro.me/admin/novels?*
// @match              https://masiro.me/admin/novelIndex
// @match              https://masiro.me/admin/novelIndex?*
// @run-at             document-idle
// @grant              GM.getValue
// @grant              GM.setValue
// @grant              GM.deleteValue
// @grant              GM.listValues
// @grant              GM.registerMenuCommand
// @grant              GM.setClipboard
// @require            https://update.greasyfork.org/scripts/483122/1304475/style-shims.js
// @require            https://update.greasyfork.org/scripts/487244/1326878/gm-import-export.js
// @require            https://unpkg.com/[email protected]/dist/i18n.object.min.js
// @require            https://update.greasyfork.org/scripts/482358/1296680/sleep.js
// @require            https://update.greasyfork.org/scripts/482311/1296481/queue.js
// @supportURL         https://greasyfork.org/scripts/471783/feedback
// ==/UserScript==

const LL = (function()
{
    const translations =
    {
        "en": {
            COMMAND: {
                IMPORT: "Import Novels Data Cache",
                EXPORT: "Export Cached Novels Data",
            },
            ERROR: {
                MALFORMED_JSON: "Malformed JSON data. Import failed.",
                UNKNOWN_ERROR: "Imported failed: {0}",
            },
            MESSAGE: {
                IMPORT_PROMPT: "Please provide JSON-formatted novels data cache:",
                IMPORT_FINISHED: "Import finished.",
                EXPORT_FINISHED: "Exported novels data cache to the clipboard.",
            },
        },
        "zh-Hant": {
            COMMAND: {
                IMPORT: "匯入小說資料快取",
                EXPORT: "匯出小說資料快取",
            },
            ERROR: {
                MALFORMED_JSON: "JSON 資料格式錯誤,匯入失敗。",
                UNKNOWN_ERROR: "匯入失敗:{0}",
            },
            MESSAGE: {
                IMPORT_PROMPT: "請提供 JSON 格式的小說資料快取:",
                IMPORT_FINISHED: "匯入完成。",
                EXPORT_FINISHED: "已匯出小說資料快取到剪貼簿。",
            },
        },
        "zh-Hans": {
            COMMAND: {
                IMPORT: "导入小说数据缓存",
                EXPORT: "导出小说数据缓存",
            },
            ERROR: {
                MALFORMED_JSON: "JSON 数据格式错误,导入失败。",
                UNKNOWN_ERROR: "导入失败:{0}",
            },
            MESSAGE: {
                IMPORT_PROMPT: "请提供 JSON 格式的小说数据缓存:",
                IMPORT_FINISHED: "导入完成。",
                EXPORT_FINISHED: "已导出小说数据缓存到剪贴板。",
            },
        },
    };

    let locale = "en";
    for (const language of navigator.languages.map((language) => new Intl.Locale(language).minimize()))
    {
        if (language.language === "zh")
        {
            locale = `zh-${language.maximize().script}`;
            break;
        }
        else if (language.baseName in Object.keys(translations))
        {
            locale = language.baseName;
            break;
        }
    }

    return i18nObject(locale, translations[locale]);
})();

GM.addStyle(`
    .updateCards > a.nsfw .updateImg, .layui-card.nsfw .n-img
    {
        filter: blur(var(--nsfw-blur-radius, 7.5px));
        transition: filter var(--nsfw-transition-duration, 0.3s);
    }

    .updateCards > a.nsfw:hover .updateImg, .updateCards > a.nsfw:focus-within .updateImg,
    .layui-card.nsfw:hover .n-img, .layui-card.nsfw:focus-within .n-img
    {
        filter: blur(0px);
    }
`);

if (GM.registerMenuCommand)
{
    GM.registerMenuCommand(LL.COMMAND.IMPORT(), () =>
    {
        setTimeout(async () =>
        {
            const cache = prompt(LL.MESSAGE.IMPORT_PROMPT(), "{}");
            if (cache)
            {
                try
                {
                    await GM.importValues(JSON.parse(cache));
                    alert(LL.MESSAGE.IMPORT_FINISHED());
                }
                catch (e)
                {
                    if (e instanceof SyntaxError)
                    {
                        console.error(e);
                        alert(LL.ERROR.MALFORMED_JSON());
                    }
                    else
                    {
                        console.error(e);
                        alert(LL.ERROR.UNKNOWN_ERROR(e?.message));
                    }
                }
            }
        }, 0);
    });

    GM.registerMenuCommand(LL.COMMAND.EXPORT(), () =>
    {
        setTimeout(async () =>
        {
            const cache = await GM.exportValues();
            GM.setClipboard(JSON.stringify(cache));

            alert(LL.MESSAGE.EXPORT_FINISHED());
        }, 0);
    });
}

const pathname = location.pathname;
if ((pathname === "/admin") || (pathname === "/admin/"))
{
    const queue = new Queue({ autostart: true, concurrency: 4 });

    const observer = new MutationObserver((records) =>
    {
        for (const record of records)
        {
            if (record.target.classList.contains("updateCards"))
            {
                for (const node of record.addedNodes)
                {
                    queue.push(async () =>
                    {
                        if (await isNsfw(node.href))
                        {
                            node.classList.add("nsfw");
                        }
                    });
                }
            }
        }
    });

    observer.observe(document.querySelector(".fl"), { subtree: true, childList: true });

    async function isNsfw(url)
    {
        const novelId = new URL(url).searchParams.get("novel_id");
        {
            const isNsfw = await GM.getValue(novelId);
            if (typeof isNsfw === "boolean") { return isNsfw; }
        }

        try
        {
            const response = await fetch(url);
            if (response.status === 200)
            {
                const html = await response.text();
                const parser = new DOMParser();
                const page = parser.parseFromString(html, "text/html");

                const isNsfw = Array.prototype.map.call(page.querySelectorAll(".tags .label"), (element) => element.innerText)
                                              .includes("R17");

                GM.setValue(novelId, isNsfw);
                return isNsfw;
            }
            else if (response.status === 429)
            {
                const resetTime = Number.parseInt(response.headers.get("x-ratelimit-reset"));
                await sleep((resetTime - Math.ceil(Date.now() / 1000) + 10) * 1000);
                return isNsfw(url);
            }
        }
        catch (e)
        {
            console.error(e);
        }

        return false;
    }
}
else
{
    const observer = new MutationObserver((records) =>
    {
        for (const record of records)
        {
            for (const node of record.addedNodes)
            {
                if ((node instanceof HTMLElement) && node.classList.contains("layui-card"))
                {
                    const isNsfw = Array.prototype.map.call(node.querySelectorAll(".tags > .tag"), (element) => element.innerText).includes("R17");
                    if (isNsfw) { node.classList.add("nsfw"); }

                    const url = new URL(node.querySelector(".glass + a").href);
                    GM.setValue(url.searchParams.get("novel_id"), isNsfw);
                }
            }
        }
    });

    observer.observe(document.querySelector(".n-leg"), { childList: true });
}