Sleazy Fork is available in English.

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.

// ==UserScript==
// @name        Remove Blacklisted Images with Sidebar
// @namespace   http://tampermonkey.net/
// @version     1.3
// @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';

    const debug = true; // Set to false to disable logging

    function getTagBlacklist() {
        const cookieValue = getCookieValue('tag_blacklist');
        const blacklist = cookieValue ? decodeBlacklist(cookieValue) : [];

        if (debug) {
            console.log('Retrieved blacklist:', blacklist);
        }

        return blacklist;
    }

    function getCookieValue(name) {
        const cookie = document.cookie.split('; ').find(row => row.startsWith(`${name}=`));
        return cookie ? cookie.split('=')[1] : null;
    }

    function decodeBlacklist(encodedString) {
        return decodeURIComponent(encodedString).split('%20');
    }

    // Function to create and inject the sidebar
    function createSidebar() {
        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" class="bi bi-caret-right-fill" 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>
    `;

        const targetElement = findSidebarInsertionTarget();
        if (!targetElement) {
            console.error('Suitable element for sidebar insertion not found.');
            return;
        }

        insertSidebar(targetElement, sidebarHTML);
        updateSidebar();
        addSidebarEventListeners();
        addCollapsibleFunctionality();
    }

    function findSidebarInsertionTarget() {
        return document.querySelector('div.tag-search') || document.querySelector('#content > h1');
    }

    function insertSidebar(targetElement, sidebarHTML) {
        const sidebarContainer = document.createElement('div');
        sidebarContainer.innerHTML = sidebarHTML;
        targetElement.insertAdjacentElement('afterend', sidebarContainer);
        if (debug) {
            console.log(`Sidebar inserted after ${targetElement.tagName.toLowerCase()} element.`);
        }
    }

    function addCollapsibleFunctionality() {
        const toggleButton = document.getElementById('toggle-header');
        const icon = toggleButton.querySelector('svg');

        toggleButton.addEventListener('click', () => {
            const content = document.getElementById('sidebar-content');
            const isCollapsed = content.style.display === 'none';
            content.style.display = isCollapsed ? 'block' : 'none';

            // Apply rotation transform
            icon.style.transform = isCollapsed ? 'rotate(90deg)' : 'rotate(0deg)';
            icon.style.transition = 'transform 0.25s ease';

            if (debug) {
                console.log('Sidebar toggle button clicked. New display state:', content.style.display);
            }
        });
    }

    // Function to update the sidebar with current blacklist items
    function updateSidebar() {
        const blacklist = getTagBlacklist();
        const blacklistList = document.getElementById('blacklist-list');
        const header = document.querySelector('#blacklist-box h2');

        blacklistList.innerHTML = '';

        const detectedTags = blacklist.filter(isTagDetectedOnPage);
        const totalHiddenPosts = updateBlacklistList(detectedTags, blacklistList);

        updateSidebarVisibility(detectedTags.length, header, totalHiddenPosts);
    }

    function updateBlacklistList(detectedTags, blacklistList) {
        let totalHiddenPosts = 0;

        detectedTags.forEach(tag => {
            const isDisabled = isTagDisabled(tag);
            const hiddenCount = getInitialHiddenPostCount(tag);
            totalHiddenPosts += hiddenCount;

            const listItem = createBlacklistListItem(tag, isDisabled, hiddenCount);
            blacklistList.appendChild(listItem);

            if (debug) console.log(`Added tag to sidebar: ${tag}, Hidden count: ${hiddenCount}`);
        });

        if (debug) console.log('Sidebar updated. Total hidden posts:', totalHiddenPosts);

        // Re-attach event listeners after updating the sidebar
        addCheckboxEventListeners();

        return totalHiddenPosts;
    }

    function createBlacklistListItem(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;
    }

    function updateSidebarVisibility(tagCount, header, totalHiddenPosts) {
        const sidebar = document.getElementById('blacklist-box');

        if (tagCount === 0) {
            sidebar.style.display = 'none';
            header.textContent = 'Blacklisted (0)';
            if (debug) console.log('No blacklisted tags detected. Sidebar hidden.');
        } else {
            sidebar.style.display = '';
            header.textContent = `Blacklisted (${totalHiddenPosts})`;
        }
    }

    // Function to check if a tag is currently detected on the page
    function isTagDetectedOnPage(tag) {
        const elements = document.querySelectorAll('img[title], div[id^="p"] > div.col1.thumb > a > img');
        return Array.from(elements).some(el => {
            const title = el.getAttribute('title');
            const isDetected = title && containsExactTag(title, [tag]);
            if (debug && isDetected) console.log(`Tag detected on page: ${tag}`);
            return isDetected;
        });
    }

    // Function to check if a tag is currently disabled
    function isTagDisabled(tag) {
        const disabledTags = JSON.parse(localStorage.getItem('disabled_tags') || '[]');
        const isDisabled = disabledTags.includes(tag);
        if (debug) console.log(`Tag ${tag} is ${isDisabled ? 'disabled' : 'enabled'}`);
        return isDisabled;
    }

    // Function to toggle the filtering state of a tag
    function toggleTag(tag, isEnabled) {
        let disabledTags = getDisabledTags();

        disabledTags = isEnabled ? removeTag(disabledTags, tag) : addTag(disabledTags, tag);

        if (debug) logTagState(tag, isEnabled);

        localStorage.setItem('disabled_tags', JSON.stringify(disabledTags));
        applyTagFiltering();
    }

    function getDisabledTags() {
        return JSON.parse(localStorage.getItem('disabled_tags') || '[]');
    }

    function removeTag(tags, tag) {
        return tags.filter(t => t !== tag);
    }

    function addTag(tags, tag) {
        if (!tags.includes(tag)) {
            tags.push(tag);
        }
        return tags;
    }

    function logTagState(tag, isEnabled) {
        const action = isEnabled ? 'enabled' : 'disabled';
        console.log(`Tag ${tag} ${action}`);
    }

    // Function to get the initial count of hidden posts for a given tag
    function getInitialHiddenPostCount(tag) {
        const favoriteCount = countHiddenPosts('img[title]', tag);
        const commentCount = countHiddenPosts('div[id^="p"] > div.col1.thumb > a > img', tag);

        const totalCount = favoriteCount + commentCount;

        if (debug) console.log(`Initial hidden post count for tag ${tag}: ${totalCount}`);
        return totalCount;
    }

    function countHiddenPosts(selector, tag) {
        let count = 0;
        const images = document.querySelectorAll(selector);
        images.forEach(img => {
            const imgTitle = img.getAttribute('title');
            if (imgTitle && containsExactTag(imgTitle, [tag])) {
                count++;
            }
        });
        return count;
    }

    // Function to check if the title contains any exact blacklist tag
    function containsExactTag(title, blacklist) {
        return blacklist.some(tag => {
            const regex = new RegExp(`\\b${tag}\\b`, 'i');
            const contains = regex.test(title);
            if (debug && contains) console.log(`Title "${title}" contains tag "${tag}"`);
            return contains;
        });
    }

    function addSidebarEventListeners() {
        const disableAllButton = document.getElementById('disable-all-blacklists');
        const reEnableAllButton = document.getElementById('re-enable-all-blacklists');

        disableAllButton.addEventListener('click', (e) => handleBlacklistToggle(e, true));
        reEnableAllButton.addEventListener('click', (e) => handleBlacklistToggle(e, false));

        // Attach checkbox event listeners
        addCheckboxEventListeners();
    }

    function handleBlacklistToggle(event, disable) {
        event.preventDefault();

        if (disable) {
            const allTags = getTagBlacklist();
            localStorage.setItem('disabled_tags', JSON.stringify(allTags));
            if (debug) console.log('Disabled all blacklisted tags');
            toggleButtonVisibility('disable-all-blacklists', 're-enable-all-blacklists');
        } else {
            localStorage.removeItem('disabled_tags');
            if (debug) console.log('Re-enabled all blacklisted tags');
            toggleButtonVisibility('re-enable-all-blacklists', 'disable-all-blacklists');
        }

        updateSidebar(); // Update the sidebar to reflect the changes
        applyTagFiltering(); // Apply filtering immediately
    }

    function toggleButtonVisibility(hiddenButtonId, visibleButtonId) {
        document.getElementById(hiddenButtonId).style.display = 'none';
        document.getElementById(visibleButtonId).style.display = 'inline';
    }

    // Function to attach event listeners to checkboxes
    function addCheckboxEventListeners() {
        document.querySelectorAll('#blacklist-list .blacklist-checkbox').forEach(checkbox => {
            checkbox.addEventListener('change', function () {
                const tag = decodeURIComponent(this.getAttribute('data-tag'));
                const isEnabled = this.checked;
                toggleTag(tag, isEnabled);
                if (debug) console.log(`Checkbox for tag "${tag}" changed to ${isEnabled ? 'enabled' : 'disabled'}`);
            });
        });
    }

    // Function to apply tag filtering to the page
    function applyTagFiltering() {
        const disabledTags = JSON.parse(localStorage.getItem('disabled_tags') || '[]');
        const allTags = getTagBlacklist();

        if (debug) console.log('Applying tag filtering. Disabled tags:', disabledTags, 'All tags:', allTags);

        // Hide posts for tags that are checked (enabled)
        hidePosts(allTags.filter(tag => !disabledTags.includes(tag)));

        // Show posts for tags that are unchecked (disabled)
        showPosts(disabledTags);
    }

    // Function to show posts for tags that should be visible
    function showPosts(tags) {
        tags.forEach(tag => updatePostsVisibility(tag, '', ''));
    }

    // Function to hide posts for tags that should be hidden
    function hidePosts(tags) {
        if (tags.length === 0) return; // If no tags to hide, exit the function.

        tags.forEach(tag => updatePostsVisibility(tag, 'none', 'important'));
    }

    // Helper function to update post visibility
    function updatePostsVisibility(tag, displayValue, importantValue) {
        const elements = document.querySelectorAll(`img[title*="${tag}"], div[id^="p"] > div.col1.thumb > a > img`);
        elements.forEach(el => {
            const title = el.getAttribute('title');
            if (title && (displayValue === '' || containsExactTag(title, [tag]))) {
                const parent = el.closest('span') || el.closest('div[id^="p"]');
                if (parent) {
                    parent.style.display = displayValue;
                    parent.style.setProperty('display', displayValue, importantValue);
                    if (debug) {
                        const action = displayValue === 'none' ? 'Hiding' : 'Showing';
                        console.log(`${action} post for tag "${tag}"`);
                    }
                }
            }
        });
    }


    // Function to remove the effect of the blacklisted-sidebar
    // Native "Hidden" button that hides/reveals images
    //TODO: think of the better logic
    function removeBlacklistedSidebarEffect() {
        handleBlacklistCount();
        removeSidebarElement();
    }

    function 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();
                if (debug) {
                    console.log('Clicked on the "Hidden" to remove the blacklisted effect');
                }
            }
        }
    }

    function removeSidebarElement() {
        const blacklistSidebar = document.getElementById('blacklisted-sidebar');
        if (blacklistSidebar) {
            blacklistSidebar.remove();
            if (debug) {
                console.log('Removed the native blacklisted-sidebar element from the page');
            }
        }
    }

    removeBlacklistedSidebarEffect();
    createSidebar();
    applyTagFiltering();
})();