Bangumi NSFW Image Blurrer

Blurs images in topics or replies marked as NSFW or by specified users on Bangumi sites.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bangumi NSFW Image Blurrer
// @version      0.1.1
// @description  Blurs images in topics or replies marked as NSFW or by specified users on Bangumi sites.
// @match        https://bgm.tv/group/*
// @match        https://bangumi.tv/group/*
// @match        https://chii.in/group/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @run-at       document-end
// @license MIT
// @namespace    bgmtv
// ==/UserScript==

(function() {
    'use strict';

    // --- Configuration ---
    const DEFAULT_BLUR_AMOUNT = '10px';
    const DEFAULT_NSFW_KEYWORDS = ['nsfw', '色图'];
    let blurAmount = DEFAULT_BLUR_AMOUNT;
    let nsfwKeywords = [];

    function loadSettings() {
        blurAmount = GM_getValue('blurAmount_bgm', DEFAULT_BLUR_AMOUNT);
        nsfwKeywords = GM_getValue('nsfwKeywords_bgm', DEFAULT_NSFW_KEYWORDS.join(',')).split(',')
            .map(keyword => keyword.trim())
            .filter(keyword => keyword);
    }

    // Add blur amount setting menu
    GM_registerMenuCommand('Set Blur Amount', () => {
        const currentAmount = GM_getValue('blurAmount_bgm', DEFAULT_BLUR_AMOUNT);
        const newAmount = prompt(
            'Enter blur amount (e.g. 5px, 10px, 15px):\nDefault: 10px',
            currentAmount
        );
        if (newAmount !== null) {
            if (/^\d+px$/.test(newAmount)) {
                GM_setValue('blurAmount_bgm', newAmount);
                blurAmount = newAmount;
                updateStyles();
                applyBlurringLogic();
            } else {
                console.warn('[BGM NSFW Protection] Invalid blur amount format. Please use format like "10px"');
                alert('Invalid format. Please use format like "10px"');
            }
        }
    });

    // Update addGlobalStyle function
    function updateStyles() {
        const styleElement = document.getElementById('nsfw-blur-style');
        if (styleElement) styleElement.remove();
        
        addGlobalStyle(`
        .nsfw-blur-userscript {
            filter: blur(${blurAmount}) !important;
            transition: filter 0.25s ease-out !important;
        }
        .nsfw-blur-userscript:hover {
            filter: blur(0px) !important;
        }
    `, 'nsfw-blur-style');
    }

    // Modified addGlobalStyle to support ID
    function addGlobalStyle(css, id = '') {
        const head = document.getElementsByTagName('head')[0];
        if (!head) {
            console.error('[BGM NSFW Protection] Cannot find head element');
            return;
        }
        const style = document.createElement('style');
        if (id) style.id = id;
        style.type = 'text/css';
        style.innerHTML = css;
        head.appendChild(style);
    }

    function loadNsfwKeywords() {
        nsfwKeywords = GM_getValue('nsfwKeywords_bgm', DEFAULT_NSFW_KEYWORDS.join(',')).split(',')
            .map(keyword => keyword.trim())
            .filter(keyword => keyword);
    }

    GM_registerMenuCommand('Set NSFW Keywords', () => {
        const currentKeywords = GM_getValue('nsfwKeywords_bgm', DEFAULT_NSFW_KEYWORDS.join(','));
        const newKeywordsInput = prompt(
            'Enter comma-separated NSFW keywords (Latin letters are case-insensitive):\nExample: nsfw,色图,R18',
            currentKeywords
        );
        if (newKeywordsInput !== null) {
            GM_setValue('nsfwKeywords_bgm', newKeywordsInput);
            loadNsfwKeywords();
            applyBlurringLogic();
        }
    });

    function checkNsfwKeywords(text) {
        return nsfwKeywords.some(keyword => {
            // For Latin letters, use case-insensitive comparison
            if (/^[a-zA-Z]+$/.test(keyword)) {
                return text.toLowerCase().includes(keyword.toLowerCase());
            }
            // For other characters (like Chinese), use case-sensitive comparison
            return text.includes(keyword);
        });
    }

    // --- Styles ---
    function addGlobalStyle(css) {
        const head = document.getElementsByTagName('head')[0];
        if (!head) { return; }
        const style = document.createElement('style');
        style.type = 'text/css';
        style.innerHTML = css;
        head.appendChild(style);
    }

    addGlobalStyle(`
        .nsfw-blur-userscript {
            filter: blur(${blurAmount}) !important;
            transition: filter 0.25s ease-out !important;
        }
        .nsfw-blur-userscript:hover {
            filter: blur(0px) !important;
        }
    `);

    // --- NSFW User ID Management ---
    let nsfwUserIds = [];

    function loadNsfwUserIds() {
        nsfwUserIds = GM_getValue('nsfwUserIds_bgm', '').split(',')
                        .map(id => id.trim().toLowerCase())
                        .filter(id => id);
    }

    GM_registerMenuCommand('Set NSFW User IDs', () => {
        const currentIds = GM_getValue('nsfwUserIds_bgm', '');
        const newIdsInput = prompt('Enter comma-separated NSFW user IDs (usernames as seen in URL, e.g., `217781` (金刚可怜)):', currentIds);
        if (newIdsInput !== null) {
            GM_setValue('nsfwUserIds_bgm', newIdsInput);
            loadNsfwUserIds();
            applyBlurringLogic(); // Re-apply logic after changing settings
        }
    });

    // --- Blurring Logic ---
    function blurImagesInElement(element) {
        if (!element) return;
        const images = element.querySelectorAll('img'); // Blurs all img tags, including emojis.
        // If you want to be more specific, e.g., only 'img.code':
        // const images = element.querySelectorAll('img.code, .topic_content > img, .message > img');
        images.forEach(img => {
            // Avoid re-blurring images in quotes if the parent is already blurred
            if (img.closest('.nsfw-blur-userscript') && !img.closest('.quote')) {
                 // If image is inside something already blurred, and not in a quote that might be safe
                 if(!img.parentElement.classList.contains('nsfw-blur-userscript')) {
                    //img.classList.add('nsfw-blur-userscript');
                 }
            } else {
                 img.classList.add('nsfw-blur-userscript');
            }
        });
    }


    function applyBlurringLogic() {
        try {
            loadNsfwUserIds();
            loadSettings();

            const topicTitleElement = document.querySelector('#pageHeader h1');
            const mainPostContainer = document.querySelector('.postTopic[data-item-user]');

            if (mainPostContainer) {
                const mainPostContentElement = mainPostContainer.querySelector('.topic_content');
                const topicTitleText = topicTitleElement ? topicTitleElement.textContent || '' : '';
                const mainPostText = mainPostContentElement ? mainPostContentElement.textContent || '' : '';
                const mainPosterIdAttr = mainPostContainer.getAttribute('data-item-user');
                const mainPosterId = mainPosterIdAttr ? mainPosterIdAttr.toLowerCase() : '';

                let topicIsMarkedNSFW = checkNsfwKeywords(topicTitleText) || checkNsfwKeywords(mainPostText);
                let mainPosterIsNSFW = mainPosterId && nsfwUserIds.includes(mainPosterId);

                if (topicIsMarkedNSFW || mainPosterIsNSFW) {
                    if (mainPostContentElement) {
                        blurImagesInElement(mainPostContentElement);
                    }
                }
            }

            // Modified replies processing
            const replies = document.querySelectorAll('.row_reply');
            replies.forEach(reply => {
                try {
                    const replyMessageElement = reply.querySelector('.reply_content .message');
                    const replyText = replyMessageElement ? replyMessageElement.textContent || '' : '';

                    // New replierID extraction logic
                    let replierId = reply.getAttribute('data-item-user');
                    if (!replierId) {
                        console.warn('[BGM NSFW Protection] Cannot find replier ID for reply:', reply);
                    }

                    let replyIsMarkedNSFW = checkNsfwKeywords(replyText);
                    let replierIsNSFW = replierId && nsfwUserIds.includes(replierId.toLowerCase());

                    if (replyIsMarkedNSFW || replierIsNSFW) {
                        if (replyMessageElement) {
                            blurImagesInElement(replyMessageElement);
                        }
                    }
                } catch (replyError) {
                    console.error('[BGM NSFW Protection] Error processing reply:', replyError);
                }
            });
        } catch (error) {
            console.error('[BGM NSFW Protection] Error in applyBlurringLogic:', error);
        }
    }

    // --- Initialization ---
    loadSettings();
    loadNsfwUserIds();
    applyBlurringLogic();

    // Optional: Re-apply on dynamic content changes (if any, might be complex)
    // const observer = new MutationObserver(applyBlurringLogic);
    // const config = { childList: true, subtree: true };
    // const targetNode = document.getElementById('main') || document.body;
    // if (targetNode) observer.observe(targetNode, config);

})();