Pixiv Block Users

Block users on Pixiv.

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

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

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

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

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

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

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

Advertisement:

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

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

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

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

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

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

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

Advertisement:

// ==UserScript==
// @name         Pixiv Block Users
// @namespace    http://tampermonkey.net/
// @version      1.2
// @description  Block users on Pixiv.
// @author       Dr.Dree
// @match        https://www.pixiv.net/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=pixiv.net
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @license      MIT

// ==/UserScript==



(function() {
    'use strict';

    const STORAGE_KEY = 'pixiv_blocked_ai_users';

    let isManageBtnVisible = GM_getValue('pb_btn_visible', true);

    GM_registerMenuCommand("Manage Button (On/Off)", () => {
        isManageBtnVisible = !isManageBtnVisible;
        GM_setValue('pb_btn_visible', isManageBtnVisible);

        const btn = document.getElementById('pb-manage-btn');
        if (btn) {
            btn.style.display = isManageBtnVisible ? 'flex' : 'none';
        }
    });

// 0. EXACT URL FILTER (Avoid tabs like /following, /illustrations...)
    function getCleanUserId(href) {
        try {
            const url = new URL(href, window.location.origin);
            // Only get URLs ending exactly with a numeric ID (e.g., /en/users/12345), ignoring trailing tabs
            const match = url.pathname.match(/^\/(?:[a-zA-Z-]+\/)?users\/(\d+)$/);
            return match ? match[1] : null;
        } catch (e) {
            return null;
        }
    }

    // 1. DATA MANAGEMENT
    function getBlockedUsers() {
        const savedData = localStorage.getItem(STORAGE_KEY);
        if (!savedData) return [];

        let parsed = JSON.parse(savedData);
        if (parsed.length > 0 && typeof parsed[0] === 'string') {
            parsed = parsed.map(id => ({ id: id, name: 'Unknown User' }));
            saveBlockedUsers(parsed);
        }
        return parsed;
    }

    function saveBlockedUsers(usersArray) {
        localStorage.setItem(STORAGE_KEY, JSON.stringify(usersArray));
    }

    // 2. MUTE UI INJECTOR
    function applyMutedUI(card) {
        // A. Replace image with Pixiv's native Mute UI
        const artworkLinks = Array.from(card.querySelectorAll('a')).filter(a => /\/artworks\/\d+/.test(a.pathname));
        if (artworkLinks.length > 0) {
            const thumbLink = artworkLinks[0];
            thumbLink.innerHTML = `
                <div style="width: 100%; height: 100%; aspect-ratio: 1 / 1; background-color: rgba(128, 128, 128, 0.1); display: flex; align-items: center; justify-content: center; border-radius: 4px; overflow: hidden;">
                    <svg viewBox="0 0 24 24" style="width: 48px; height: 48px; fill: currentColor; opacity: 0.3;">
                        <path d="M5.26763775,4 L9.38623853,11.4134814 L5,14.3684211 L5,18 L13.0454155,18 L14.1565266,20 L5,20 C3.8954305,20 3,19.1045695 3,18 L3,6 C3,4.8954305 3.8954305,4 5,4 L5.26763775,4 Z M9.84347336,4 L19,4 C20.1045695,4 21,4.8954305 21,6 L21,18 C21,19.1045695 20.1045695,20 19,20 L18.7323623,20 L17.6212511,18 L19,18 L19,13 L16,15 L15.9278695,14.951913 L9.84347336,4 Z M16,7 C14.8954305,7 14,7.8954305 14,9 C14,10.1045695 14.8954305,11 16,11 C17.1045695,11 18,10.1045695 18,9 C18,7.8954305 17.1045695,7 16,7 Z M7.38851434,1.64019979 L18.3598002,21.3885143 L16.6114857,22.3598002 L5.64019979,2.61148566 L7.38851434,1.64019979 Z"></path>
                    </svg>
                </div>
            `;
            thumbLink.onclick = (e) => e.preventDefault();
            thumbLink.style.cursor = 'default';

            // B. Change title text to "Muted"
            if (artworkLinks.length > 1) {
                const titleLink = artworkLinks[1];
                titleLink.textContent = 'Muted';
                titleLink.style.opacity = '0.5';
                titleLink.onclick = (e) => e.preventDefault();
                titleLink.style.cursor = 'default';
            }
        }

        // C. Safely hide avatar and author name (no more while-loop DOM climbing)
        const userLinksInCard = card.querySelectorAll('a[href*="/users/"]');
        userLinksInCard.forEach(link => {
            if (getCleanUserId(link.href)) {
                // Hide the adjacent block button (if any)
                if (link.nextElementSibling && link.nextElementSibling.title === 'Add this author to the blocklist') {
                    link.nextElementSibling.style.display = 'none';
                }
                // Hide name/avatar
                link.style.display = 'none';
            }
        });

        // D. Hide Bookmark button (Heart icon)
        const buttons = card.querySelectorAll('button');
        buttons.forEach(btn => btn.style.display = 'none');
    }

    // 3. CORE LOGIC: SMART FINDER
    function muteBlockedArtworks() {
        const blockedUsers = getBlockedUsers();
        if (blockedUsers.length === 0) return;

        const blockedIds = blockedUsers.map(user => user.id);
        const userLinks = document.querySelectorAll('a[href*="/users/"]');

        userLinks.forEach(link => {
            const userId = getCleanUserId(link.href);
            // Skip if not a valid user link or user is not blocked
            if (!userId || !blockedIds.includes(userId)) return;

            let card = link.closest('li');

            if (!card) {
                let parent = link.parentElement;
                for (let i = 0; i < 7; i++) {
                    if (!parent) break;
                    // Must contain an <a> tag leading to a SPECIFIC ARTWORK (/artworks/id) and contain an image
                    const hasArtThumbnail = Array.from(parent.querySelectorAll('a')).some(a =>
                        /\/artworks\/\d+$/.test(a.pathname) && (a.querySelector('img') || a.querySelector('[role="img"]'))
                    );

                    if (hasArtThumbnail) {
                        card = parent;
                        break;
                    }
                    parent = parent.parentElement;
                }
            }

            // SAFEGUARD FOR PROFILE PAGES
            // Ensure not to hide large blocks (Body, Main) and strictly DO NOT HIDE tags containing Headings (h1, h2)
            if (card && card.tagName !== 'BODY' && card.tagName !== 'MAIN') {
                const linkCount = card.querySelectorAll('a').length;
                const hasHeader = card.querySelector('h1, h2'); // The name on the profile header is usually an H1 or H2 tag

                if (linkCount < 20 && !hasHeader && !card.classList.contains('pb-muted-done')) {
                    card.classList.add('pb-muted-done');
                    applyMutedUI(card);
                }
            }
        });
    }

    // 4. INJECT BLOCK BUTTONS [🚫]
    function addBlockButtons() {
        const userLinks = document.querySelectorAll('a[href*="/users/"]:not(.has-block-btn)');

        userLinks.forEach(link => {
            link.classList.add('has-block-btn');

            // 1. Check for clean ID (Filter out tab links, info links)
            const userId = getCleanUserId(link.href);
            if (!userId) return;

            // 2. Skip if the link is inside a navigation bar (<nav>) (Filter out Home, Illustrations tabs)
            if (link.closest('nav')) return;

            // 3. Skip if it is an Avatar image
            if (link.querySelector('img') || link.querySelector('[role="img"]') || link.querySelector('svg')) {
                return;
            }

            const userName = link.textContent.trim();
            // 4. Skip if text is empty or accidentally matches system keywords
            if (!userName || /^(Home|Illustrations|Following|Followers)$/i.test(userName)) return;

            const btn = document.createElement('span');
            btn.innerHTML = ' 🚫';
            btn.title = 'Add this author to the blocklist';
            btn.style.cursor = 'pointer';
            btn.style.fontSize = '12px';
            btn.style.marginLeft = '4px';
            btn.style.opacity = '0.7';

            btn.onmouseover = () => btn.style.opacity = '1';
            btn.onmouseout = () => btn.style.opacity = '0.7';

            btn.onclick = function(e) {
                e.preventDefault();
                e.stopPropagation();

                let currentList = getBlockedUsers();

                if (!currentList.some(user => user.id === userId)) {
                    currentList.push({ id: userId, name: userName });
                    saveBlockedUsers(currentList);

                    console.log(`[Pixiv Blocker] Muted: ${userName} (ID: ${userId})`);
                    muteBlockedArtworks();
                }
            };

            link.parentNode.insertBefore(btn, link.nextSibling);
        });
    }

    // 5. VISUAL MANAGER UI
    function injectUI() {
        if (document.getElementById('pb-manage-btn')) return;

        const style = document.createElement('style');
        style.innerHTML = `
            #pb-manage-btn {
                position: fixed; bottom: 20px; left: 20px;
                background: rgba(128, 128, 128, 0.8);
                color: white; border: none;
                width: 45px; height: 45px; border-radius: 50%;
                cursor: pointer; z-index: 9998; font-size: 20px;
                display: flex; align-items: center; justify-content: center;
                box-shadow: 0 2px 5px rgba(0,0,0,0.3); transition: 0.3s;
            }
            #pb-manage-btn:hover { background: rgba(100, 100, 100, 1); }

            #pb-modal-overlay { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0,0,0,0.6); z-index: 9999; display: none; align-items: center; justify-content: center; font-family: sans-serif; }
            #pb-modal-content { background: #fff; color: #333; padding: 20px; border-radius: 8px; width: 450px; max-height: 80vh; display: flex; flex-direction: column; box-shadow: 0 4px 15px rgba(0,0,0,0.3); }
            #pb-modal-header { font-size: 18px; font-weight: bold; margin-bottom: 15px; border-bottom: 1px solid #ddd; padding-bottom: 10px; display: flex; justify-content: space-between; }
            #pb-user-list { overflow-y: auto; flex-grow: 1; margin-bottom: 15px; padding-right: 5px; }
            .pb-user-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; border-bottom: 1px solid #eee; }
            .pb-user-info { display: flex; flex-direction: column; }
            .pb-user-name { font-weight: bold; font-size: 14px; }
            .pb-user-id { font-size: 12px; color: #888; }

            .pb-unblock-btn { background: #ff4d4f; color: white; border: none; padding: 5px 10px; border-radius: 4px; cursor: pointer; font-size: 12px; }
            .pb-unblock-btn:hover { background: #ff7875; }

            #pb-modal-footer { display: flex; justify-content: flex-end; gap: 10px; border-top: 1px solid #ddd; padding-top: 15px; }
            #pb-close-btn { background: #f0f0f0; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; color: #333; font-weight: bold; }
            #pb-close-btn:hover { background: #e0e0e0; }
            #pb-save-reload-btn { background: #0096fa; color: white; border: none; padding: 10px 15px; border-radius: 4px; cursor: pointer; font-weight: bold; }
            #pb-save-reload-btn:hover { background: #0077c8; }
        `;
        document.head.appendChild(style);

        const manageBtn = document.createElement('button');
        manageBtn.id = 'pb-manage-btn';
        manageBtn.innerHTML = '🛡️';
        manageBtn.title = 'Manage Blocklist';
        manageBtn.style.display = isManageBtnVisible ? 'flex' : 'none';
        document.body.appendChild(manageBtn);

        const modalOverlay = document.createElement('div');
        modalOverlay.id = 'pb-modal-overlay';

        const modalContent = document.createElement('div');
        modalContent.id = 'pb-modal-content';

        modalContent.innerHTML = `
            <div id="pb-modal-header">
                <span>Blocked Authors</span>
                <span id="pb-count" style="font-size: 14px; font-weight: normal; color: #666;">0 users</span>
            </div>
            <div id="pb-user-list"></div>
            <div id="pb-modal-footer">
                <button id="pb-close-btn">Close</button>
                <button id="pb-save-reload-btn">Save & Reload</button>
            </div>
        `;

        modalOverlay.appendChild(modalContent);
        document.body.appendChild(modalOverlay);

        const userListDiv = modalContent.querySelector('#pb-user-list');
        const countSpan = modalContent.querySelector('#pb-count');

        let tempBlockedUsers = [];

        function renderList() {
            countSpan.innerText = `${tempBlockedUsers.length} users`;
            userListDiv.innerHTML = '';

            if (tempBlockedUsers.length === 0) {
                userListDiv.innerHTML = '<div style="text-align: center; color: #999; margin-top: 20px;">No blocked authors yet.</div>';
                return;
            }

            tempBlockedUsers.forEach(user => {
                const item = document.createElement('div');
                item.className = 'pb-user-item';
                item.innerHTML = `
                    <div class="pb-user-info">
                        <span class="pb-user-name">${user.name}</span>
                        <span class="pb-user-id">ID: ${user.id}</span>
                    </div>
                    <button class="pb-unblock-btn" data-id="${user.id}">Unblock</button>
                `;
                userListDiv.appendChild(item);
            });

            modalContent.querySelectorAll('.pb-unblock-btn').forEach(btn => {
                btn.onclick = function() {
                    const idToRemove = this.getAttribute('data-id');
                    tempBlockedUsers = tempBlockedUsers.filter(u => u.id !== idToRemove);
                    renderList();
                };
            });
        }

        manageBtn.onclick = function() {
            tempBlockedUsers = getBlockedUsers();
            renderList();
            modalOverlay.style.display = 'flex';
        };

        modalContent.querySelector('#pb-close-btn').onclick = function() {
            modalOverlay.style.display = 'none';
        };

        modalContent.querySelector('#pb-save-reload-btn').onclick = function() {
            saveBlockedUsers(tempBlockedUsers);
            modalOverlay.style.display = 'none';
            window.location.reload();
        };
    }

    // 6. MAIN RUNNER
    function runApp() {
        addBlockButtons();
        muteBlockedArtworks();
        injectUI();
    }

    runApp();

    const observer = new MutationObserver(() => {
        runApp();
    });

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

})();