Block users on Pixiv.
// ==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
});
})();