Greasy Fork

Greasy Fork is available in English.

隐藏NSFW

避免网站NSFW图片直接展示到电脑屏幕

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         隐藏NSFW
// @namespace    http://tampermonkey.net/
// @version      25.02.04
// @description  避免网站NSFW图片直接展示到电脑屏幕
// @author       Rawwiin
// @match        *://*/*
// @icon         https://img.icons8.com/?size=100&id=85344&format=png&color=000000

// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand

// @license MIT

// ==/UserScript==

(function () {
    'use strict';

    // ==================== 工具函数 ====================

    // 从数组中移除指定值(替代 Array.prototype.remove)
    function arrayRemove(arr, val) {
        let index;
        while ((index = arr.indexOf(val)) > -1) {
            arr.splice(index, 1);
        }
        return arr;
    }

    // DOM Ready 替代 $(document).ready
    function domReady(fn) {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', fn);
        } else {
            fn();
        }
    }

    // ==================== 配置 ====================

    let hpop_config_custom;
    const hpop_config_default = {
        version: "25.02.04",
        sitesEnabled: [],      // 启用的网站列表(只有在列表中的网站才生效)
        hideOpacity: 10,       // 隐藏透明度 (1-100),1=几乎不可见, 100=完全可见
        grayscale: false,      // 图片灰度模式
        pageGrayscale: false   // 网页灰度模式
    };

    // 当前隐藏状态(用于动态内容)
    let isCurrentlyHidden = false;

    // MutationObserver 实例
    let observer = null;

    // 缓存当前域名(包含端口)
    const currentHost = document.location.host;

    // ==================== 样式 ====================

    const STYLE_RAW = `
        /* CSS 变量定义 */
        :root {
            --hpop-hide-opacity: 0.1;
        }
        /* 隐藏媒体元素 */
        .hpop-transparent-image {
            opacity: var(--hpop-hide-opacity) !important;
            transition: opacity 0.2s ease, filter 0.2s ease;
        }
        .hpop-transparent-image:hover {
            opacity: 1 !important;
        }
        /* 图片灰度模式 */
        .hpop-grayscale-image {
            filter: grayscale(100%) !important;
            transition: filter 0.2s ease;
        }
        /* 隐藏+灰度模式:悬停时保持灰度模式 */
        .hpop-transparent-image.hpop-grayscale-image:hover {
            filter: grayscale(100%) !important;
        }
        /* 网页灰度模式 */
        .hpop-page-grayscale {
            filter: grayscale(100%) !important;
        }
    `;

    // 更新隐藏透明度 CSS 变量
    function updateHideOpacity() {
        const opacity = hpop_config_custom.hideOpacity / 100;
        document.documentElement.style.setProperty('--hpop-hide-opacity', opacity);
    }

    // ==================== 初始化 ====================

    function init() {
        GM_addStyle(STYLE_RAW);

        // 取出本地缓存配置
        hpop_config_custom = GM_getValue("hpop_config") || { ...hpop_config_default };

        // 将数据结构的变更保存到本地缓存配置
        let updFlag = false;
        for (const _key in hpop_config_default) {
            if (!hpop_config_custom.hasOwnProperty(_key)) {
                hpop_config_custom[_key] = hpop_config_default[_key];
                updFlag = true;
            }
        }
        if (updFlag) {
            GM_setValue("hpop_config", hpop_config_custom);
        }

        // 注册菜单
        menu_Func_regist();

        // 初始化透明度 CSS 变量
        updateHideOpacity();

        // 检查当前网站是否启用
        const isEnabled = hpop_config_custom.sitesEnabled.includes(currentHost);

        // 只有当前网站在启用列表中才应用效果
        if (isEnabled) {
            // 应用隐藏图片
            domReady(() => imgHide());

            // 应用图片灰度模式
            if (hpop_config_custom.grayscale) {
                domReady(() => applyGrayscale());
            }

            // 应用网页灰度模式
            if (hpop_config_custom.pageGrayscale) {
                domReady(() => applyPageGrayscale());
            }
        }

        // 启动 MutationObserver 监听动态内容
        startObserver();

        console.log('[隐藏NSFW]', hpop_config_custom);
    }

    // ==================== MutationObserver ====================

    // 待处理的节点集合(使用 Set 避免重复)
    let pendingNodes = new Set();
    let rafScheduled = false;
    let scanTimer = null;

    // 隐藏单个媒体元素(img/video,同时处理灰度模式效果)
    function hideMedia(el) {
        if (!el || !el.classList) return;
        
        // 应用隐藏样式
        if (!el.classList.contains('hpop-transparent-image')) {
            el.classList.add('hpop-transparent-image');
        }
        
        // 应用灰度模式样式(如果开启)
        if (hpop_config_custom.grayscale && !el.classList.contains('hpop-grayscale-image')) {
            el.classList.add('hpop-grayscale-image');
        }
    }

    // 仅应用灰度模式效果到单个媒体元素
    function applyGrayscaleToMedia(el) {
        if (!el || !el.classList) return;
        if (hpop_config_custom.grayscale && !el.classList.contains('hpop-grayscale-image')) {
            el.classList.add('hpop-grayscale-image');
        }
    }

    // 批量处理待处理节点
    function processPendingNodes() {
        const shouldHide = isCurrentlyHidden;
        const shouldGrayscale = hpop_config_custom.grayscale;
        
        if ((!shouldHide && !shouldGrayscale) || pendingNodes.size === 0) {
            rafScheduled = false;
            return;
        }

        const nodes = Array.from(pendingNodes);
        pendingNodes.clear();
        rafScheduled = false;

        nodes.forEach(node => {
            if (node.nodeType !== Node.ELEMENT_NODE) return;

            // 处理 img、video 和 iframe 元素
            if (node.tagName === 'IMG' || node.tagName === 'VIDEO' || node.tagName === 'IFRAME') {
                if (shouldHide) {
                    hideMedia(node);
                } else if (shouldGrayscale) {
                    applyGrayscaleToMedia(node);
                }
            } else if (node.querySelectorAll) {
                const medias = node.querySelectorAll('img, video, iframe');
                medias.forEach(el => {
                    if (shouldHide) {
                        hideMedia(el);
                    } else if (shouldGrayscale) {
                        applyGrayscaleToMedia(el);
                    }
                });
            }
        });
    }

    // 使用 requestAnimationFrame 调度处理
    function scheduleProcess() {
        if (!rafScheduled) {
            rafScheduled = true;
            requestAnimationFrame(processPendingNodes);
        }
    }

    // 全量扫描页面媒体元素(处理漏网之鱼)
    function scanAllMedia() {
        const shouldHide = isCurrentlyHidden;
        const shouldGrayscale = hpop_config_custom.grayscale;
        
        if (!shouldHide && !shouldGrayscale) return;
        
        if (shouldHide) {
            const medias = document.querySelectorAll('img:not(.hpop-transparent-image), video:not(.hpop-transparent-image), iframe:not(.hpop-transparent-image)');
            medias.forEach(hideMedia);
        } else if (shouldGrayscale) {
            const medias = document.querySelectorAll('img:not(.hpop-grayscale-image), video:not(.hpop-grayscale-image), iframe:not(.hpop-grayscale-image)');
            medias.forEach(applyGrayscaleToMedia);
        }
    }

    // 启动定期扫描(处理某些复杂场景)
    function startPeriodicScan() {
        if (scanTimer) return;
        // 每 500ms 扫描一次,确保没有遗漏
        scanTimer = setInterval(() => {
            scanAllMedia();
        }, 500);
    }

    // 停止定期扫描
    function stopPeriodicScan() {
        if (scanTimer) {
            clearInterval(scanTimer);
            scanTimer = null;
        }
    }

    function startObserver() {
        if (observer) return;

        observer = new MutationObserver(mutations => {
            const shouldHide = isCurrentlyHidden;
            const shouldGrayscale = hpop_config_custom.grayscale;
            
            // 如果两个模式都没开启,直接返回
            if (!shouldHide && !shouldGrayscale) return;

            for (const mutation of mutations) {
                // 处理新增节点
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            pendingNodes.add(node);
                        }
                    });
                }
                // 处理属性变化(懒加载媒体 src 变化)
                if (mutation.type === 'attributes' && (mutation.target.tagName === 'IMG' || mutation.target.tagName === 'VIDEO' || mutation.target.tagName === 'IFRAME')) {
                    if (shouldHide) {
                        hideMedia(mutation.target);
                    } else if (shouldGrayscale) {
                        applyGrayscaleToMedia(mutation.target);
                    }
                }
            }

            // 调度处理
            if (pendingNodes.size > 0) {
                scheduleProcess();
            }
        });

        // 开始观察整个文档
        const target = document.body || document.documentElement;
        observer.observe(target, {
            childList: true,
            subtree: true,
            attributes: true,
            attributeFilter: ['src', 'data-src', 'srcset'] // 监听懒加载相关属性
        });
    }

    // ==================== 菜单功能 ====================

    function menu_Func_regist() {
        // 只在顶层窗口注册菜单,避免 iframe 中的 CDN 域名也生成菜单
        if (window.self !== window.top) return;

        const isEnabled = hpop_config_custom.sitesEnabled.includes(currentHost);

        // 1. 网站生效菜单(放在最前面,因为这是最常用的)
        GM_registerMenuCommand(
            `${isEnabled ? '✅' : '❌'} 当前网站生效 (${currentHost})`,
            function () {
                if (isEnabled) {
                    // 取消生效
                    arrayRemove(hpop_config_custom.sitesEnabled, currentHost);
                    // 立即显示图片
                    imgShow();
                    // 移除图片灰度模式
                    const imgs = document.querySelectorAll('.hpop-grayscale-image');
                    imgs.forEach(img => img.classList.remove('hpop-grayscale-image'));
                    // 移除网页灰度模式
                    document.documentElement.classList.remove('hpop-page-grayscale');
                } else {
                    // 添加生效
                    hpop_config_custom.sitesEnabled.push(currentHost);
                    // 立即应用所有效果
                    imgHide();
                    if (hpop_config_custom.grayscale) {
                        applyGrayscale();
                    }
                    if (hpop_config_custom.pageGrayscale) {
                        applyPageGrayscale();
                    }
                }

                GM_setValue("hpop_config", hpop_config_custom);
                menu_Func_regist();
            },
            {
                id: "menu_enable_site",
                accessKey: "e",
                autoClose: true
            }
        );

        // 2. 隐藏透明度菜单
        GM_registerMenuCommand(
            `👁️ 隐藏透明度: ${hpop_config_custom.hideOpacity}%`,
            function () {
                const input = prompt(
                    '请输入隐藏透明度 (1-100)\n\n1 = 几乎不可见\n50 = 半透明\n100 = 完全可见',
                    hpop_config_custom.hideOpacity
                );

                if (input === null) return; // 用户取消

                const value = parseInt(input, 10);
                if (isNaN(value) || value < 1 || value > 100) {
                    alert('请输入 1 到 100 之间的数字');
                    return;
                }

                hpop_config_custom.hideOpacity = value;
                updateHideOpacity();

                // 如果当前站点已启用,立即应用
                if (hpop_config_custom.sitesEnabled.includes(currentHost)) {
                    imgHide();
                }

                GM_setValue("hpop_config", hpop_config_custom);
                menu_Func_regist();
            },
            {
                id: "menu_hide_opacity",
                accessKey: "o",
                autoClose: false
            }
        );

        // 3. 图片灰度模式菜单
        GM_registerMenuCommand(
            `${hpop_config_custom.grayscale ? '✅' : '❌'} 图片灰度模式`,
            function () {
                hpop_config_custom.grayscale = !hpop_config_custom.grayscale;
                
                // 如果当前站点已启用,立即应用
                if (hpop_config_custom.sitesEnabled.includes(currentHost)) {
                    applyGrayscale();
                }
                
                GM_setValue("hpop_config", hpop_config_custom);
                menu_Func_regist();
            },
            {
                id: "menu_grayscale",
                accessKey: "g",
                autoClose: true
            }
        );

        // 4. 网页灰度模式菜单
        GM_registerMenuCommand(
            `${hpop_config_custom.pageGrayscale ? '✅' : '❌'} 网页灰度模式`,
            function () {
                hpop_config_custom.pageGrayscale = !hpop_config_custom.pageGrayscale;
                
                // 如果当前站点已启用,立即应用
                if (hpop_config_custom.sitesEnabled.includes(currentHost)) {
                    applyPageGrayscale();
                }
                
                GM_setValue("hpop_config", hpop_config_custom);
                menu_Func_regist();
            },
            {
                id: "menu_page_grayscale",
                accessKey: "p",
                autoClose: true
            }
        );
    }

    // 应用/取消媒体灰度模式效果
    function applyGrayscale() {
        const medias = document.querySelectorAll('img, video, iframe');
        if (hpop_config_custom.grayscale) {
            medias.forEach(el => el.classList.add('hpop-grayscale-image'));
            // 如果没有开启隐藏模式,也需要启动定期扫描以处理动态媒体
            if (!isCurrentlyHidden) {
                startPeriodicScan();
            }
        } else {
            medias.forEach(el => el.classList.remove('hpop-grayscale-image'));
            // 如果隐藏模式也没开启,停止定期扫描
            if (!isCurrentlyHidden) {
                stopPeriodicScan();
            }
        }
    }

    // 应用/取消网页灰度模式效果
    function applyPageGrayscale() {
        const html = document.documentElement;
        if (hpop_config_custom.pageGrayscale) {
            html.classList.add('hpop-page-grayscale');
        } else {
            html.classList.remove('hpop-page-grayscale');
        }
    }

    // ==================== 媒体显示/隐藏 ====================

    function imgHide() {
        isCurrentlyHidden = true;
        // 隐藏所有媒体元素
        scanAllMedia();
        // 启动定期扫描
        startPeriodicScan();
    }

    function imgShow() {
        isCurrentlyHidden = false;
        // 如果灰度模式没有开启,停止定期扫描
        if (!hpop_config_custom.grayscale) {
            stopPeriodicScan();
        }
        // 显示所有媒体(移除隐藏样式,保留灰度模式样式)
        const medias = document.querySelectorAll('.hpop-transparent-image');
        medias.forEach(el => el.classList.remove('hpop-transparent-image'));
    }

    // ==================== 启动 ====================

    init();
})();