Greasy Fork is available in English.
避免网站NSFW图片直接展示到电脑屏幕
// ==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();
})();