Remove Blacklisted Images with Sidebar

Adds a sidebar to manage and display blacklisted tags and hides or shows images based on the blacklist on rule34.xxx.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==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();
    }
})();