您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add NSFW mask over images in Discord Web, reveal on hover.
当前为
// ==UserScript== // @name Discord Image NSFW Mask // @namespace http://tampermonkey.net/ // @version 0.3 // @description Add NSFW mask over images in Discord Web, reveal on hover. // @match https://discord.com/* // @grant none // @author chatgpt // @license WTFPL // ==/UserScript== (function() { 'use strict'; const style = document.createElement("style"); style.textContent = ` .nsfw-mask { position: relative; /* For absolute positioning of overlay */ display: block; /* Make it block to fill parent width */ width: 100%; /* Fill parent width */ height: 100%; /* Fill parent height (assumes parent has dimensions) */ overflow: hidden; /* Hide anything from img that might overflow if object-fit is cover */ } .nsfw-mask img { filter: blur(8px); transition: filter 0.3s ease-in-out; display: block; /* Standard for images within containers */ width: 100%; /* Make image fill the mask */ height: 100%; /* Make image fill the mask */ object-fit: cover; /* Cover the area, might crop if aspect ratios differ */ } .nsfw-overlay { position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0, 0, 0, 0.65); /* Slightly darker */ color: white; display: flex; justify-content: center; align-items: center; font-weight: bold; font-size: 12px; text-align: center; pointer-events: none; /* Crucial */ opacity: 1; transition: opacity 0.3s ease-in-out; border-radius: 3px; /* Optional: match Discord's image border-radius */ box-sizing: border-box; } /* ---- KEY CHANGE HERE ---- */ /* Target a stable Discord wrapper for hover, then affect children */ /* Using [class*="clickableWrapper"]:hover is more resilient to class name changes */ /* You can also use the specific class if it's stable enough for you: .clickableWrapper_af017a:hover */ div[class*="clickableWrapper"]:hover .nsfw-mask img, div[class*="imageWrapper"]:hover .nsfw-mask img /* Fallback for other structures if clickableWrapper is not present */ { filter: none; } div[class*="clickableWrapper"]:hover .nsfw-mask .nsfw-overlay, div[class*="imageWrapper"]:hover .nsfw-mask .nsfw-overlay { opacity: 0; } /* Old hover rules (can be removed or kept as fallback if the above are too specific) */ /* .nsfw-mask:hover img { filter: none; } .nsfw-mask:hover .nsfw-overlay { opacity: 0; } */ `; document.head.appendChild(style); function processImage(img) { if (img.closest('.nsfw-mask')) return; // Already processed // The direct parent of the image is often a div like 'loadingOverlay...' or similar const imageParent = img.parentNode; if (!imageParent) return; const wrapper = document.createElement('div'); wrapper.className = 'nsfw-mask'; // If the original image had a specific display style we might want to respect, // but for this setup, nsfw-mask is block and 100% width/height. // const originalDisplay = window.getComputedStyle(img).display; // if (originalDisplay === 'block') { // wrapper.style.display = 'block'; // } const overlay = document.createElement('div'); overlay.className = 'nsfw-overlay'; overlay.textContent = 'NSFW - Hover'; // DOM manipulation: // imageParent -> img BECOMES imageParent -> wrapper -> img, overlay imageParent.insertBefore(wrapper, img); // Insert wrapper before img in its original parent wrapper.appendChild(img); // Move the image inside the wrapper wrapper.appendChild(overlay); // Add the overlay inside the wrapper } const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'childList' && mutation.addedNodes.length > 0) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // Check if the added node is an image or contains images const images = []; if (node.tagName === 'IMG') { images.push(node); } else { node.querySelectorAll('img').forEach(img => images.push(img)); } images.forEach(img => { if (!img.closest('.nsfw-mask') && img.src && (img.src.includes('/attachments/') || img.src.includes('cdn.discordapp.com/ephemeral-attachments/')) && !img.closest('a[href^="/channels/"][href*="/users/"]') && !img.classList.contains('emoji') && !img.closest('[class*="avatar"]') && !img.closest('[class*="reaction"]') && img.offsetWidth > 16 && img.offsetHeight > 16) { // Basic check for actual images not tiny icons // Ensure the image is somewhat loaded or use a timeout if (img.complete || (img.offsetWidth > 0 && img.offsetHeight > 0)) { processImage(img); } else { img.onload = () => processImage(img); // Fallback timeout if onload doesn't fire (e.g. broken image, or already loaded before handler attached) setTimeout(() => { if (!img.closest('.nsfw-mask')) processImage(img); }, 300); } } }); } }); } } }); observer.observe(document.body, { childList: true, subtree: true }); // Initial run for images already present on load // Use a timeout to allow Discord to finish its initial rendering setTimeout(() => { document.querySelectorAll('img').forEach(img => { if (!img.closest('.nsfw-mask') && img.src && (img.src.includes('/attachments/') || img.src.includes('cdn.discordapp.com/ephemeral-attachments/')) && !img.closest('a[href^="/channels/"][href*="/users/"]') && !img.classList.contains('emoji') && !img.closest('[class*="avatar"]') && !img.closest('[class*="reaction"]') && img.offsetWidth > 16 && img.offsetHeight > 16) { processImage(img); } }); }, 1000); // Increased timeout for initial scan })();