您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Adds a sidebar to manage and display blacklisted tags and hides or shows images based on the blacklist on rule34.xxx.
// ==UserScript== // @name Remove Blacklisted Images with Sidebar // @namespace http://tampermonkey.net/ // @version 1.5 // @description Adds a sidebar to manage and display blacklisted tags and hides or shows images based on the blacklist on rule34.xxx. // @author Dramorian // @match https://rule34.xxx/index.php?page=post* // @match https://rule34.xxx/index.php?page=favorites* // @match https://rule34.xxx/index.php?page=comment* // @exclude https://rule34.xxx/index.php?page=post&s=view* // @icon https://www.google.com/s2/favicons?sz=64&domain=rule34.xxx // @grant none // ==/UserScript== (function() { 'use strict'; // Configuration const CONFIG = { DEBUG: true, SELECTORS: { IMAGES: 'img[title], div[id^="p"] > div.col1.thumb > a > img', FAVORITES: 'img[title]', COMMENTS: 'div[id^="p"] > div.col1.thumb > a > img', SIDEBAR_TARGET: 'div.tag-search', SIDEBAR_FALLBACK: '#content > h1' }, STORAGE_KEYS: { DISABLED_TAGS: 'disabled_tags', TAG_BLACKLIST: 'tag_blacklist' } }; // Utility functions const Logger = { log: (...args) => CONFIG.DEBUG && console.log('[Blacklist Manager]', ...args), error: (...args) => console.error('[Blacklist Manager]', ...args) }; const Utils = { getCookie(name) { const cookie = document.cookie .split('; ') .find(row => row.startsWith(`${name}=`)); return cookie ? cookie.split('=')[1] : null; }, decodeBlacklist(encodedString) { return decodeURIComponent(encodedString).split('%20'); }, createElementFromHTML(html) { const container = document.createElement('div'); container.innerHTML = html.trim(); return container.firstChild; }, debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } }; // Storage manager class StorageManager { static getDisabledTags() { try { return JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEYS.DISABLED_TAGS) || '[]'); } catch (e) { Logger.error('Failed to parse disabled tags from localStorage:', e); return []; } } static setDisabledTags(tags) { try { localStorage.setItem(CONFIG.STORAGE_KEYS.DISABLED_TAGS, JSON.stringify(tags)); } catch (e) { Logger.error('Failed to save disabled tags to localStorage:', e); } } static clearDisabledTags() { localStorage.removeItem(CONFIG.STORAGE_KEYS.DISABLED_TAGS); } static isTagDisabled(tag) { return this.getDisabledTags().includes(tag); } } // Blacklist manager class BlacklistManager { static getTagBlacklist() { const cookieValue = Utils.getCookie(CONFIG.STORAGE_KEYS.TAG_BLACKLIST); const blacklist = cookieValue ? Utils.decodeBlacklist(cookieValue) : []; Logger.log('Retrieved blacklist:', blacklist); return blacklist; } static containsExactTag(title, tags) { return tags.some(tag => { const regex = new RegExp(`\\b${tag}\\b`, 'i'); const contains = regex.test(title); if (contains) Logger.log(`Title "${title}" contains tag "${tag}"`); return contains; }); } static isTagDetectedOnPage(tag) { const elements = document.querySelectorAll(CONFIG.SELECTORS.IMAGES); return Array.from(elements).some(el => { const title = el.getAttribute('title'); return title && this.containsExactTag(title, [tag]); }); } static countPostsWithTag(tag) { const favoriteCount = this._countPostsBySelector(CONFIG.SELECTORS.FAVORITES, tag); const commentCount = this._countPostsBySelector(CONFIG.SELECTORS.COMMENTS, tag); const total = favoriteCount + commentCount; Logger.log(`Post count for tag ${tag}: ${total}`); return total; } static _countPostsBySelector(selector, tag) { const images = document.querySelectorAll(selector); return Array.from(images).filter(img => { const title = img.getAttribute('title'); return title && this.containsExactTag(title, [tag]); }).length; } } // Post visibility manager class PostVisibilityManager { static updatePostsVisibility(tag, displayValue, important = false) { const elements = document.querySelectorAll(CONFIG.SELECTORS.IMAGES); elements.forEach(el => { const title = el.getAttribute('title'); if (!title) return; const shouldUpdate = displayValue === '' || BlacklistManager.containsExactTag(title, [tag]); if (shouldUpdate) { const parent = el.closest('span') || el.closest('div[id^="p"]'); if (parent) { parent.style.display = displayValue; if (important) { parent.style.setProperty('display', displayValue, 'important'); } const action = displayValue === 'none' ? 'Hiding' : 'Showing'; Logger.log(`${action} post for tag "${tag}"`); } } }); } static showPostsWithTags(tags) { tags.forEach(tag => this.updatePostsVisibility(tag, '')); } static hidePostsWithTags(tags) { if (tags.length === 0) return; tags.forEach(tag => this.updatePostsVisibility(tag, 'none', true)); } static applyFiltering() { const disabledTags = StorageManager.getDisabledTags(); const allTags = BlacklistManager.getTagBlacklist(); const enabledTags = allTags.filter(tag => !disabledTags.includes(tag)); Logger.log('Applying filtering - Disabled:', disabledTags, 'Enabled:', enabledTags); this.hidePostsWithTags(enabledTags); this.showPostsWithTags(disabledTags); } } // Sidebar manager class SidebarManager { constructor() { this.sidebar = null; this.isCollapsed = true; } create() { const targetElement = this._findInsertionTarget(); if (!targetElement) { Logger.error('Suitable element for sidebar insertion not found.'); return; } this.sidebar = this._createSidebarElement(); targetElement.insertAdjacentElement('afterend', this.sidebar); this._attachEventListeners(); this.update(); Logger.log('Sidebar created and initialized'); } _findInsertionTarget() { return document.querySelector(CONFIG.SELECTORS.SIDEBAR_TARGET) || document.querySelector(CONFIG.SELECTORS.SIDEBAR_FALLBACK); } _createSidebarElement() { const sidebarHTML = ` <div id="blacklist-box"> <div id="sidebar-header"> <h2>Blacklisted</h2> <button id="toggle-header" aria-label="Toggle Sidebar"> <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16" class="toggle-icon"> <path d="m12.14 8.753-5.482 4.796c-.646.566-1.658.106-1.658-.753V3.204a1 1 0 0 1 1.659-.753l5.48 4.796a1 1 0 0 1 0 1.506z"/> </svg> </button> </div> <div id="sidebar-content" style="display: none;"> <ul id="blacklist-list" style="list-style: none;"></ul> </div> <div id="sidebar-footer"> <button id="disable-all-blacklists">Disable All</button> <button id="re-enable-all-blacklists" style="display: none;">Re-enable All</button> </div> </div> `; return Utils.createElementFromHTML(sidebarHTML); } _attachEventListeners() { // Toggle button const toggleButton = this.sidebar.querySelector('#toggle-header'); const icon = toggleButton.querySelector('svg'); toggleButton.addEventListener('click', () => { this._toggleCollapse(icon); }); // Control buttons const disableAllBtn = this.sidebar.querySelector('#disable-all-blacklists'); const enableAllBtn = this.sidebar.querySelector('#re-enable-all-blacklists'); disableAllBtn.addEventListener('click', (e) => { e.preventDefault(); this._disableAllTags(); }); enableAllBtn.addEventListener('click', (e) => { e.preventDefault(); this._enableAllTags(); }); } _toggleCollapse(icon) { const content = this.sidebar.querySelector('#sidebar-content'); this.isCollapsed = !this.isCollapsed; content.style.display = this.isCollapsed ? 'none' : 'block'; icon.style.transform = this.isCollapsed ? 'rotate(0deg)' : 'rotate(90deg)'; icon.style.transition = 'transform 0.25s ease'; Logger.log('Sidebar toggled:', this.isCollapsed ? 'collapsed' : 'expanded'); } _disableAllTags() { const allTags = BlacklistManager.getTagBlacklist(); StorageManager.setDisabledTags(allTags); this._toggleButtons(true); this.update(); PostVisibilityManager.applyFiltering(); Logger.log('All tags disabled'); } _enableAllTags() { StorageManager.clearDisabledTags(); this._toggleButtons(false); this.update(); PostVisibilityManager.applyFiltering(); Logger.log('All tags enabled'); } _toggleButtons(allDisabled) { const disableBtn = this.sidebar.querySelector('#disable-all-blacklists'); const enableBtn = this.sidebar.querySelector('#re-enable-all-blacklists'); disableBtn.style.display = allDisabled ? 'none' : 'inline'; enableBtn.style.display = allDisabled ? 'inline' : 'none'; } update() { if (!this.sidebar) return; const blacklist = BlacklistManager.getTagBlacklist(); const detectedTags = blacklist.filter(tag => BlacklistManager.isTagDetectedOnPage(tag) ); this._updateTagList(detectedTags); this._updateVisibility(detectedTags); } _updateTagList(detectedTags) { const listElement = this.sidebar.querySelector('#blacklist-list'); listElement.innerHTML = ''; let totalHiddenPosts = 0; detectedTags.forEach(tag => { const isDisabled = StorageManager.isTagDisabled(tag); const hiddenCount = BlacklistManager.countPostsWithTag(tag); if (!isDisabled) { totalHiddenPosts += hiddenCount; } const listItem = this._createTagListItem(tag, isDisabled, hiddenCount); listElement.appendChild(listItem); }); // Update header with total count const header = this.sidebar.querySelector('h2'); header.textContent = `Blacklisted (${totalHiddenPosts})`; // Attach checkbox listeners this._attachCheckboxListeners(); } _createTagListItem(tag, isDisabled, hiddenCount) { const listItem = document.createElement('li'); listItem.innerHTML = ` <label> <input type="checkbox" class="blacklist-checkbox" data-tag="${encodeURIComponent(tag)}" ${isDisabled ? '' : 'checked'}> ${tag} <span class="count">${hiddenCount}</span> </label> `; return listItem; } _attachCheckboxListeners() { const checkboxes = this.sidebar.querySelectorAll('.blacklist-checkbox'); checkboxes.forEach(checkbox => { checkbox.addEventListener('change', (e) => { const tag = decodeURIComponent(e.target.getAttribute('data-tag')); const isEnabled = e.target.checked; this._toggleTag(tag, isEnabled); Logger.log(`Tag "${tag}" ${isEnabled ? 'enabled' : 'disabled'}`); }); }); } _toggleTag(tag, isEnabled) { let disabledTags = StorageManager.getDisabledTags(); if (isEnabled) { disabledTags = disabledTags.filter(t => t !== tag); } else { if (!disabledTags.includes(tag)) { disabledTags.push(tag); } } StorageManager.setDisabledTags(disabledTags); PostVisibilityManager.applyFiltering(); } _updateVisibility(detectedTags) { const sidebar = this.sidebar; if (detectedTags.length === 0) { sidebar.style.display = 'none'; Logger.log('No blacklisted tags detected. Sidebar hidden.'); } else { sidebar.style.display = ''; } } } // Native sidebar cleanup class NativeSidebarManager { static removeNativeEffects() { this._handleBlacklistCount(); this._removeSidebarElement(); } static _handleBlacklistCount() { const blacklistCountElement = document.getElementById('blacklist-count'); if (!blacklistCountElement) return; const postCount = parseInt(blacklistCountElement.textContent, 10); if (postCount > 0) { const hiddenLink = blacklistCountElement .closest('h5') ?.querySelector('a'); if (hiddenLink) { hiddenLink.click(); Logger.log('Clicked native "Hidden" button to remove blacklist effect'); } } } static _removeSidebarElement() { const blacklistSidebar = document.getElementById('blacklisted-sidebar'); if (blacklistSidebar) { blacklistSidebar.remove(); Logger.log('Removed native blacklisted-sidebar element'); } } } // Main application class BlacklistApp { constructor() { this.sidebarManager = new SidebarManager(); } init() { try { // Clean up native effects first NativeSidebarManager.removeNativeEffects(); // Create and initialize sidebar this.sidebarManager.create(); // Apply initial filtering PostVisibilityManager.applyFiltering(); Logger.log('Blacklist application initialized successfully'); } catch (error) { Logger.error('Failed to initialize application:', error); } } } // Initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => { new BlacklistApp().init(); }); } else { new BlacklistApp().init(); } })();