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