Greasy Fork

Greasy Fork is available in English.

真白萌:模糊 R17 封面

模糊真白萌 R17 小说的封面。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==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.icu/scripts/483122/1304475/style-shims.js
// @require            https://update.greasyfork.icu/scripts/487244/1326878/gm-import-export.js
// @require            https://unpkg.com/[email protected]/dist/i18n.object.min.js
// @require            https://update.greasyfork.icu/scripts/482358/1296680/sleep.js
// @require            https://update.greasyfork.icu/scripts/482311/1296481/queue.js
// @supportURL         http://greasyfork.icu/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 });
}