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.

Per 04-09-2024. Zie de nieuwste versie.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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