您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Hides backlinks pointing to hidden posts, prevents hover tooltips and adds strikethrough to quotelinks, and adds recursive hiding/filtering options on 8chan.moe/se. Also adds unhiding options.
- // ==UserScript==
- // @name 8chan Hiding Enhancer
- // @namespace nipah-scripts-8chan
- // @version 1.5.1
- // @description Hides backlinks pointing to hidden posts, prevents hover tooltips and adds strikethrough to quotelinks, and adds recursive hiding/filtering options on 8chan.moe/se. Also adds unhiding options.
- // @author nipah, Gemini
- // @license MIT
- // @match https://8chan.moe/*/res/*.html*
- // @match https://8chan.se/*/res/*.html*
- // @grant none
- // @run-at document-idle
- // ==/UserScript==
- (function() {
- 'use strict';
- const SCRIPT_NAME = 'Hiding Enhancer';
- const BACKLINK_SELECTOR = '.panelBacklinks a, .altBacklinks a';
- const QUOTE_LINK_SELECTOR = '.quoteLink';
- const ALL_LINK_SELECTORS = `${BACKLINK_SELECTOR}, ${QUOTE_LINK_SELECTOR}`;
- const POST_CONTAINER_SELECTOR = '.opCell, .postCell';
- const INNER_POST_SELECTOR = '.innerOP, .innerPost'; // Selector for the inner content div
- const THREAD_CONTAINER_SELECTOR = '#divThreads'; // Container for all posts in the thread
- const HIDDEN_CLASS = 'hidden'; // Class added to the container when hidden by hiding.js
- const HIDDEN_QUOTE_CLASS = 'hidden-quote'; // Class to add to quote links for hidden posts
- const TOOLTIP_SELECTOR = '.quoteTooltip'; // Selector for the tooltip element
- const HIDE_BUTTON_SELECTOR = '.hideButton'; // Selector for the hide menu button
- const HIDE_MENU_SELECTOR = '.floatingList.extraMenu'; // Selector for the hide menu dropdown
- const LABEL_ID_SELECTOR = '.labelId'; // Selector for the post ID label
- const UNHIDE_BUTTON_SELECTOR = '.unhideButton'; // Selector for the site's unhide button
- const log = (...args) => console.log(`[${SCRIPT_NAME}]`, ...args);
- const warn = (...args) => console.warn(`[${SCRIPT_NAME}]`, ...args);
- const error = (...args) => console.error(`[${SCRIPT_NAME}]`, ...args);
- let debounceTimer = null;
- const DEBOUNCE_DELAY = 250; // ms
- /**
- * Injects custom CSS styles into the document head.
- */
- function addCustomStyles() {
- const style = document.createElement('style');
- style.type = 'text/css';
- style.innerHTML = `
- .${HIDDEN_QUOTE_CLASS} {
- text-decoration: line-through !important;
- }
- /* Style for the dynamically added menu items */
- ${HIDE_MENU_SELECTOR} li[data-action^="hide-recursive"],
- ${HIDE_MENU_SELECTOR} li[data-action^="filter-id-recursive"],
- ${HIDE_MENU_SELECTOR} li[data-action="show-id"],
- ${HIDE_MENU_SELECTOR} li[data-action="show-all"] {
- cursor: pointer;
- }
- `;
- document.head.appendChild(style);
- log('Custom styles injected.');
- }
- /**
- * Extracts the target post ID from a link's href attribute.
- * Works for both backlinks and quote links.
- * @param {HTMLAnchorElement} linkElement - The link <a> element.
- * @returns {string|null} The target post ID as a string, or null if not found.
- */
- function getTargetPostIdFromLink(linkElement) {
- if (!linkElement || !linkElement.href) {
- return null;
- }
- // Match the post number after the last '#'
- const match = linkElement.href.match(/#(\d+)$/);
- // Only return numeric post ID
- return match ? match[1] : null;
- }
- /**
- * Checks if a post is currently hidden based on its ID.
- * @param {string} postId - The ID of the post to check.
- * @returns {boolean} True if the post is hidden, false otherwise.
- */
- function isPostHidden(postId) {
- if (!postId) return false;
- const postContainer = document.getElementById(postId);
- if (!postContainer) return false;
- // Check if the main container (.opCell or .postCell) is hidden (can happen with thread hiding)
- if (postContainer.classList.contains(HIDDEN_CLASS)) {
- return true;
- }
- // Check if the inner content container (.innerOP or .innerPost) is hidden (common for post hiding)
- const innerContent = postContainer.querySelector(INNER_POST_SELECTOR);
- return innerContent ? innerContent.classList.contains(HIDDEN_CLASS) : false;
- }
- /**
- * Updates the visibility or style of a single link based on its target post's hidden status.
- * Handles both backlinks and quote links.
- * @param {HTMLAnchorElement} linkElement - The link <a> element to update.
- */
- function updateLinkVisibility(linkElement) {
- const targetPostId = getTargetPostIdFromLink(linkElement);
- // Ensure it's a numeric post ID link
- if (!targetPostId) return;
- const hidden = isPostHidden(targetPostId);
- if (linkElement.classList.contains('quoteLink')) {
- // It's a quote link, apply strikethrough
- if (hidden) {
- linkElement.classList.add(HIDDEN_QUOTE_CLASS);
- // // log(`Adding strikethrough to quote link ${linkElement.href} pointing to hidden post ${targetPostId}`);
- } else {
- linkElement.classList.remove(HIDDEN_QUOTE_CLASS);
- // // log(`Removing strikethrough from quote link ${linkElement.href} pointing to visible post ${targetPostId}`);
- }
- } else {
- // It's a backlink, hide/show the element
- if (hidden) {
- linkElement.style.display = 'none';
- // // log(`Hiding backlink ${linkElement.href} pointing to hidden post ${targetPostId}`);
- } else {
- // Reset display.
- linkElement.style.display = '';
- // // log(`Showing backlink ${linkElement.href} pointing to visible post ${targetPostId}`);
- }
- }
- }
- /**
- * Iterates through all relevant links (backlinks and quote links) on the page and updates their visibility/style.
- */
- function updateAllLinks() {
- log('Updating all link visibility/style...');
- const links = document.querySelectorAll(ALL_LINK_SELECTORS);
- links.forEach(updateLinkVisibility);
- log(`Checked ${links.length} links.`);
- }
- /**
- * Debounced version of updateAllLinks.
- */
- function debouncedUpdateAllLinks() {
- clearTimeout(debounceTimer);
- debounceTimer = setTimeout(updateAllLinks, DEBOUNCE_DELAY);
- }
- /**
- * Overrides the site's tooltips.loadTooltip function to prevent tooltips for hidden posts.
- */
- function overrideLoadTooltip() {
- // Check if tooltips object and loadTooltip function exist
- if (typeof tooltips === 'undefined' || typeof tooltips.loadTooltip !== 'function') {
- // Not ready, try again later
- setTimeout(overrideLoadTooltip, 100);
- return;
- }
- const originalLoadTooltip = tooltips.loadTooltip;
- tooltips.loadTooltip = function(tooltip, quoteUrl, replyId, isInline) {
- // Only intercept hover tooltips (isInline is false for hover tooltips)
- if (!isInline) {
- const matches = quoteUrl.match(/\/(\w+)\/res\/(\d+)\.html\#(\d+)/);
- const targetPostId = matches ? matches[3] : null; // Post ID is the 3rd group
- if (targetPostId && isPostHidden(targetPostId)) {
- log(`Preventing hover tooltip for quote to hidden post ${targetPostId}`);
- // Remove the tooltip element that was just created by the site's code
- if (tooltip && tooltip.parentNode) {
- tooltip.parentNode.removeChild(tooltip);
- }
- // Clear the site's internal reference if it points to the removed tooltip
- // This is important so tooltips.removeIfExists doesn't try to remove it again
- if (tooltips.currentTooltip === tooltip) {
- tooltips.currentTooltip = null;
- }
- // Prevent the original function from running
- return;
- }
- }
- // If it's an inline quote OR the target post is not hidden, call the original function
- originalLoadTooltip.apply(this, arguments);
- };
- log('tooltips.loadTooltip overridden to prevent tooltips for hidden posts.');
- }
- /**
- * Implements the recursive hiding logic using the site's hiding.hidePost function.
- * Hides a specific post and all its replies recursively.
- * @param {string} startPostId - The ID of the post to start hiding from.
- */
- function hidePostAndRepliesRecursivelyUserscript(startPostId) {
- // Ensure site objects are available
- if (typeof hiding === 'undefined' || typeof hiding.hidePost !== 'function' || typeof tooltips === 'undefined' || typeof tooltips.knownPosts === 'undefined') {
- error('Site hiding or tooltips objects not available. Cannot perform recursive hide.');
- return;
- }
- const boardUri = window.location.pathname.split('/')[1]; // Get boardUri from URL
- function recursiveHide(currentPostId) {
- const postElement = document.getElementById(currentPostId);
- if (!postElement) {
- // Post element not found (might be filtered or not loaded)
- // log(`Post element ${currentPostId} not found.`);
- return;
- }
- // Check if the post already has the site's unhide button, indicating it's already hidden
- if (postElement.querySelector(UNHIDE_BUTTON_SELECTOR)) {
- // log(`Post ${currentPostId} is already hidden (unhide button found). Skipping.`);
- } else {
- const linkSelf = postElement.querySelector('.linkSelf');
- if (linkSelf) {
- log(`Hiding post ${currentPostId}`);
- // Call the site's hidePost function
- hiding.hidePost(linkSelf);
- } else {
- warn(`Could not find .linkSelf for post ${currentPostId}. Cannot hide.`);
- }
- }
- // Find replies using the site's tooltips.knownPosts structure
- const knownPost = tooltips.knownPosts[boardUri]?.[currentPostId];
- if (!knownPost || !knownPost.added || knownPost.added.length === 0) {
- // log(`No known replies for post ${currentPostId}. Stopping recursion.`);
- return; // No replies or post not found in knownPosts
- }
- // Recursively hide replies
- knownPost.added.forEach((replyString) => {
- const [replyBoard, replyId] = replyString.split('_');
- // Only hide replies within the same board and thread
- // The site's knownPosts structure seems to only track replies within the same thread anyway
- if (replyBoard === boardUri) {
- recursiveHide(replyId);
- }
- });
- }
- // Start the recursive hiding process
- log(`Starting recursive hide from post ${startPostId}`);
- recursiveHide(startPostId);
- log(`Finished recursive hide from post ${startPostId}`);
- // After hiding is done, trigger a link update to reflect changes
- debouncedUpdateAllLinks();
- }
- /**
- * Implements the recursive filtering logic.
- * Adds an ID filter and recursively hides replies for all posts matching that ID.
- * @param {string} targetId - The raw ID string (e.g., '0feed1') to filter by.
- * @param {string} clickedPostId - The ID of the post whose menu was clicked (used for context/logging).
- */
- function filterIdAndHideAllMatchingAndReplies(targetId, clickedPostId) {
- // Ensure site objects are available
- if (typeof settingsMenu === 'undefined' || typeof settingsMenu.createFilter !== 'function' || typeof hiding === 'undefined' || typeof hiding.hidePost !== 'function' || typeof tooltips === 'undefined' || typeof tooltips.knownPosts === 'undefined' || typeof hiding.buildPostFilterId !== 'function') {
- error('Site settingsMenu, hiding, tooltips, or hiding.buildPostFilterId objects not available. Cannot perform recursive ID filter.');
- return;
- }
- const boardUri = window.location.pathname.split('/')[1];
- const threadId = window.location.pathname.split('/')[3].split('.')[0]; // Extract thread ID from URL
- // Find the linkSelf element for the clicked post to pass to buildPostFilterId
- const clickedPostElement = document.getElementById(clickedPostId);
- let formattedFilterString = targetId; // Fallback to raw ID
- if (clickedPostElement) {
- const linkSelf = clickedPostElement.querySelector('.linkSelf');
- if (linkSelf) {
- // Use the site's function to get the formatted ID string
- formattedFilterString = hiding.buildPostFilterId(linkSelf, targetId);
- } else {
- warn(`Could not find .linkSelf for clicked post ${clickedPostId}. Using raw ID for filter.`);
- }
- } else {
- warn(`Could not find clicked post element ${clickedPostId}. Using raw ID for filter.`);
- }
- log(`Applying Filter ID++ for ID: ${targetId} (formatted as "${formattedFilterString}") triggered from post ${clickedPostId})`);
- // 1. Add the ID filter using the site's function
- // Type 4 is for filtering by ID
- settingsMenu.createFilter(formattedFilterString, false, 4);
- log(`Added filter for ID: ${formattedFilterString}`);
- // Give the site's filter logic a moment to apply the 'hidden' class
- // Then find all posts with this ID and recursively hide their replies
- setTimeout(() => {
- const allPosts = document.querySelectorAll(POST_CONTAINER_SELECTOR);
- allPosts.forEach(postElement => {
- const postIdLabel = postElement.querySelector(LABEL_ID_SELECTOR);
- const currentPostId = postElement.id;
- // Check if the post matches the target ID
- if (postIdLabel && postIdLabel.textContent === targetId) {
- log(`Found post ${currentPostId} matching ID ${targetId}. Recursively hiding its replies.`);
- // Call the recursive hide function starting from this post.
- // hidePostAndRepliesRecursivelyUserscript will handle hiding the post itself
- // (if not already hidden by the filter) and its replies.
- hidePostAndRepliesRecursivelyUserscript(currentPostId);
- }
- });
- // After hiding is done, trigger a link update to reflect changes
- // This is already handled by hidePostAndRepliesRecursivelyUserscript,
- // but calling it again here after the loop ensures all changes are caught.
- debouncedUpdateAllLinks();
- }, DEBOUNCE_DELAY + 50); // Wait slightly longer than the debounce delay
- }
- /**
- * Removes all filters associated with a specific raw ID from the site's settings.
- * @param {string} targetId - The raw ID string (e.g., '0feed1') to remove filters for.
- * @param {string} clickedPostId - The ID of the post whose menu was clicked (used for context/logging).
- */
- function removeIdFilters(targetId, clickedPostId) {
- // Ensure site objects are available
- if (typeof settingsMenu === 'undefined' || typeof settingsMenu.loadedFilters === 'undefined' || typeof hiding === 'undefined' || typeof hiding.checkFilters !== 'function' || typeof hiding.buildPostFilterId !== 'function') {
- error('Site settingsMenu, hiding, or hiding.buildPostFilterId objects not available. Cannot remove ID filters.');
- return;
- }
- const boardUri = window.location.pathname.split('/')[1];
- const threadId = window.location.pathname.split('/')[3].split('.')[0]; // Extract thread ID from URL
- // Find the linkSelf element for the clicked post to pass to buildPostFilterId
- const clickedPostElement = document.getElementById(clickedPostId);
- let formattedFilterString = targetId; // Fallback to raw ID
- if (clickedPostElement) {
- const linkSelf = clickedPostElement.querySelector('.linkSelf');
- if (linkSelf) {
- // Use the site's function to get the formatted ID string
- formattedFilterString = hiding.buildPostFilterId(linkSelf, targetId);
- } else {
- warn(`Could not find .linkSelf for clicked post ${clickedPostId}. Using raw ID for filter removal check.`);
- }
- } else {
- warn(`Could not find clicked post element ${clickedPostId}. Using raw ID for filter removal check.`);
- }
- log(`Attempting to remove filters for ID: ${targetId} (formatted as "${formattedFilterString}") triggered from post ${clickedPostId})`);
- // Filter out the matching filters
- const initialFilterCount = settingsMenu.loadedFilters.length;
- settingsMenu.loadedFilters = settingsMenu.loadedFilters.filter(filter => {
- // Check if it's an ID filter (type 4 or 5) and if the filter content matches the formatted ID string
- return !( (filter.type === 4 || filter.type === 5) && filter.filter === formattedFilterString );
- });
- const removedCount = initialFilterCount - settingsMenu.loadedFilters.length;
- if (removedCount > 0) {
- log(`Removed ${removedCount} filter(s) for ID: ${formattedFilterString}`);
- // Update localStorage
- localStorage.setItem('filterData', JSON.stringify(settingsMenu.loadedFilters));
- // Trigger the site's filter update
- hiding.checkFilters();
- log('Triggered site filter update.');
- } else {
- log(`No filters found for ID: ${formattedFilterString} to remove.`);
- }
- // After removing filters, trigger a link update to reflect changes (posts might become visible)
- debouncedUpdateAllLinks();
- }
- /**
- * Removes all ID filters and manual hides for the current thread.
- */
- function showAllInThread() {
- // Ensure site objects are available
- if (typeof settingsMenu === 'undefined' || typeof settingsMenu.loadedFilters === 'undefined' || typeof hiding === 'undefined' || typeof hiding.checkFilters !== 'function' || typeof hiding.buildPostFilterId !== 'function') {
- error('Site settingsMenu, hiding, or hiding.buildPostFilterId objects not available. Cannot show all in thread.');
- return;
- }
- const boardUri = window.location.pathname.split('/')[1];
- const threadId = window.location.pathname.split('/')[3].split('.')[0]; // Extract thread ID from URL
- log(`Attempting to show all posts in thread /${boardUri}/res/${threadId}.html`);
- let filtersRemoved = 0;
- let unhideButtonsClicked = 0;
- // 1. Find and click all existing unhide buttons in the current thread
- log('Searching for and clicking existing unhide buttons...');
- const allPostsInThread = document.querySelectorAll(POST_CONTAINER_SELECTOR);
- allPostsInThread.forEach(postElement => {
- const postId = postElement.id;
- if (!postId) return; // Skip if element has no ID
- let unhideButton = null;
- if (postId === threadId) {
- // For the thread (OP), the button is the previous sibling
- unhideButton = postElement.previousElementSibling;
- if (!unhideButton || !unhideButton.matches(UNHIDE_BUTTON_SELECTOR)) {
- unhideButton = null; // Reset if not found or doesn't match
- }
- } else {
- // For regular posts, the button is inside the post container
- unhideButton = postElement.querySelector(UNHIDE_BUTTON_SELECTOR);
- }
- if (unhideButton) {
- log(`Clicking unhide button for ${postId}`);
- unhideButton.click();
- unhideButtonsClicked++;
- }
- });
- log(`Clicked ${unhideButtonsClicked} unhide button(s).`);
- // 2. Remove ID filters specific to this thread from settingsMenu
- const initialFilterCount = settingsMenu.loadedFilters.length;
- settingsMenu.loadedFilters = settingsMenu.loadedFilters.filter(filter => {
- // Check if it's an ID filter (type 4 or 5) and if the filter content starts with the board-thread prefix
- const isThreadIdFilter = (filter.type === 4 || filter.type === 5) && filter.filter.startsWith(`${boardUri}-${threadId}-`);
- if (isThreadIdFilter) {
- filtersRemoved++;
- }
- return !isThreadIdFilter;
- });
- if (filtersRemoved > 0) {
- log(`Removed ${filtersRemoved} ID filter(s) specific to this thread.`);
- // Update localStorage for filters
- localStorage.setItem('filterData', JSON.stringify(settingsMenu.loadedFilters));
- } else {
- log('No ID filters specific to this thread found to remove.');
- }
- // 3. Trigger the site's filter update AFTER a short delay to allow button clicks to process
- // and for the filter removal to take effect.
- setTimeout(() => {
- hiding.checkFilters();
- log('Triggered site filter update after delay.');
- // 5. Trigger userscript link update
- debouncedUpdateAllLinks();
- log('Finished "Show All" action.');
- }, 100); // 100ms delay
- }
- /**
- * Adds the custom "Hide post++", "Filter ID++", "Show ID", and "Show All" options to a hide button's menu when it appears.
- * Uses a MutationObserver to detect when the menu is added to the button.
- * @param {HTMLElement} hideButton - The hide button element.
- */
- function addCustomHideMenuOptions(hideButton) {
- // Create a new observer for each hide button
- // This observer will stay active for the lifetime of the hideButton element
- const observer = new MutationObserver((mutationsList) => {
- for (const mutation of mutationsList) {
- if (mutation.type === 'childList') {
- for (const addedNode of mutation.addedNodes) {
- // Check if the added node is the menu we're looking for
- if (addedNode.nodeType === Node.ELEMENT_NODE && addedNode.matches(HIDE_MENU_SELECTOR)) {
- // Check if this menu is a child of the target hideButton
- if (hideButton.contains(addedNode)) {
- // Menu appeared, now add the custom options if they're not already there
- const menuUl = addedNode.querySelector('ul');
- if (menuUl) {
- const postContainer = hideButton.closest(POST_CONTAINER_SELECTOR);
- const postId = postContainer ? postContainer.id : null;
- const isOP = postContainer ? postContainer.classList.contains('opCell') : false;
- const postIdLabel = postContainer ? postContainer.querySelector(LABEL_ID_SELECTOR) : null;
- const postIDText = postIdLabel ? postIdLabel.textContent : null;
- // Find anchor points for insertion
- const hidePostPlusItem = Array.from(menuUl.querySelectorAll('li')).find(li => li.textContent.trim() === 'Hide post+');
- const filterIdPlusItem = Array.from(menuUl.querySelectorAll('li')).find(li => li.textContent.trim() === 'Filter ID+');
- // Keep track of the last item we inserted after
- let lastInsertedAfter = null;
- // --- Add "Hide post++" ---
- // Only add for reply posts and if it doesn't exist
- if (!isOP && postId && !menuUl.querySelector('li[data-action="hide-recursive"]')) {
- const hideRecursiveItem = document.createElement('li');
- hideRecursiveItem.textContent = 'Hide post++';
- hideRecursiveItem.dataset.action = 'hide-recursive';
- hideRecursiveItem.addEventListener('click', (event) => {
- log(`'Hide post++' clicked for post ${postId}`);
- hidePostAndRepliesRecursivelyUserscript(postId);
- });
- // Insert after "Hide post+" if found
- if (hidePostPlusItem) {
- hidePostPlusItem.after(hideRecursiveItem);
- lastInsertedAfter = hideRecursiveItem;
- log(`Added 'Hide post++' option after 'Hide post+' for post ${postId}.`);
- } else {
- // Fallback: append to the end if "Hide post+" isn't found (shouldn't happen for replies)
- menuUl.appendChild(hideRecursiveItem);
- lastInsertedAfter = hideRecursiveItem;
- warn(`'Hide post+' not found for post ${postId}. Appended 'Hide post++' to end.`);
- }
- }
- // --- Add "Filter ID++" ---
- // Only add if the post has an ID and it doesn't exist
- if (postIDText && !menuUl.querySelector('li[data-action="filter-id-recursive"]')) {
- const filterIdRecursiveItem = document.createElement('li');
- filterIdRecursiveItem.textContent = 'Filter ID++';
- filterIdRecursiveItem.dataset.action = 'filter-id-recursive';
- filterIdRecursiveItem.addEventListener('click', (event) => {
- filterIdAndHideAllMatchingAndReplies(postIDText, postId);
- });
- // Insert after "Filter ID+" if it exists, otherwise after the last item we added ("Hide post++")
- if (filterIdPlusItem) {
- filterIdPlusItem.after(filterIdRecursiveItem);
- lastInsertedAfter = filterIdRecursiveItem;
- log(`Added 'Filter ID++' option after 'Filter ID+' for post ${postId}.`);
- } else if (lastInsertedAfter) { // If Hide post++ was added
- lastInsertedAfter.after(filterIdRecursiveItem);
- lastInsertedAfter = filterIdRecursiveItem;
- warn(`'Filter ID+' not found for post ${postId}. Appended 'Filter ID++' after last added item.`);
- } else {
- // Fallback: append to the end if neither "Filter ID+" nor "Hide post++" were present/added
- menuUl.appendChild(filterIdRecursiveItem);
- lastInsertedAfter = filterIdRecursiveItem;
- warn(`Neither 'Filter ID+' nor previous custom item found for post ${postId}. Appended 'Filter ID++' to end.`);
- }
- }
- // --- Add "Show ID" ---
- // Only add if the post has an ID and it doesn't exist
- if (postIDText && !menuUl.querySelector('li[data-action="show-id"]')) {
- const showIdItem = document.createElement('li');
- showIdItem.textContent = 'Show ID';
- showIdItem.dataset.action = 'show-id';
- showIdItem.addEventListener('click', (event) => {
- removeIdFilters(postIDText, postId);
- // Simulate click outside to close menu via site's logic
- setTimeout(() => document.body.click(), 0);
- });
- // Insert after the last item we added ("Filter ID++" or "Hide post++")
- if (lastInsertedAfter) {
- lastInsertedAfter.after(showIdItem);
- lastInsertedAfter = showIdItem;
- log(`Added 'Show ID' option after last added custom item for post ${postId}.`);
- } else if (filterIdPlusItem) {
- // Fallback if no custom items were added before this, but "Filter ID+" exists
- filterIdPlusItem.after(showIdItem);
- lastInsertedAfter = showIdItem;
- warn(`No previous custom item found for post ${postId}. Appended 'Show ID' after 'Filter ID+'.`);
- } else {
- // Fallback: append to the end if nothing else was added/found
- menuUl.appendChild(showIdItem);
- lastInsertedAfter = showIdItem;
- warn(`Neither previous custom item nor 'Filter ID+' found for post ${postId}. Appended 'Show ID' to end.`);
- }
- }
- // --- Add "Show All" ---
- // Add this option regardless of post type or ID, if it doesn't exist
- if (!menuUl.querySelector('li[data-action="show-all"]')) {
- const showAllItem = document.createElement('li');
- showAllItem.textContent = 'Show All';
- showAllItem.dataset.action = 'show-all';
- showAllItem.addEventListener('click', (event) => {
- log(`'Show All' clicked for post ${postId}`);
- showAllInThread();
- // Simulate click outside to close menu via site's logic
- setTimeout(() => document.body.click(), 0);
- });
- // Insert after the last item we added ("Show ID", "Filter ID++", or "Hide post++")
- if (lastInsertedAfter) {
- lastInsertedAfter.after(showAllItem);
- } else {
- // Fallback: append to the end if no other custom items were added
- menuUl.appendChild(showAllItem);
- }
- log(`Added 'Show All' option for post ${postId}.`);
- }
- } else {
- warn('Could not find ul inside hide menu.');
- }
- }
- }
- }
- }
- }
- });
- // Start observing the hide button for added children (the menu appears as a child)
- observer.observe(hideButton, { childList: true });
- }
- /**
- * Finds all existing hide buttons on the page and attaches the menu observer logic.
- */
- function addCustomHideOptionsToExistingButtons() {
- const hideButtons = document.querySelectorAll(HIDE_BUTTON_SELECTOR);
- hideButtons.forEach(addCustomHideMenuOptions);
- log(`Attached menu observers to ${hideButtons.length} existing hide buttons.`);
- }
- // --- Initialization ---
- log('Initializing...');
- // Add custom CSS styles
- addCustomStyles();
- // Initial setup after a short delay to ensure site scripts are ready
- setTimeout(() => {
- updateAllLinks(); // Update links based on initial hidden posts
- overrideLoadTooltip(); // Override tooltip function
- addCustomHideOptionsToExistingButtons(); // Add menu options to posts already on the page
- }, 500);
- // Observe changes in the thread container to catch new posts or visibility changes
- const threadContainer = document.querySelector(THREAD_CONTAINER_SELECTOR);
- if (threadContainer) {
- const observer = new MutationObserver((mutationsList) => {
- let needsLinkUpdate = false;
- for (const mutation of mutationsList) {
- // Check for class changes on post containers (.opCell, .postCell) or their inner content (.innerOP, .innerPost)
- if (mutation.type === 'attributes' && mutation.attributeName === 'class' && (mutation.target.matches(POST_CONTAINER_SELECTOR) || mutation.target.matches(INNER_POST_SELECTOR))) {
- const wasHidden = mutation.oldValue ? mutation.oldValue.includes(HIDDEN_CLASS) : false;
- const isHidden = mutation.target.classList.contains(HIDDEN_CLASS);
- if (wasHidden !== isHidden) {
- const postContainer = mutation.target.closest(POST_CONTAINER_SELECTOR);
- const postId = postContainer ? postContainer.id : 'unknown';
- log(`Mutation: Class change on post ${postId}. Hidden: ${isHidden}. Triggering link update.`);
- needsLinkUpdate = true;
- }
- }
- // Check for new nodes being added
- else if (mutation.type === 'childList') {
- mutation.addedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- // If a post container is added directly
- if (node.matches(POST_CONTAINER_SELECTOR)) {
- log(`Mutation: New post container added (ID: ${node.id}). Triggering link update and adding menu observer.`);
- needsLinkUpdate = true;
- const hideButton = node.querySelector(HIDE_BUTTON_SELECTOR);
- if (hideButton) {
- addCustomHideMenuOptions(hideButton); // Attach observer to the new hide button
- }
- } else {
- // Check for post containers within the added node's subtree
- const newPosts = node.querySelectorAll(POST_CONTAINER_SELECTOR);
- if (newPosts.length > 0) {
- log(`Mutation: New posts added within subtree. Triggering link update and adding menu observers.`);
- needsLinkUpdate = true;
- newPosts.forEach(post => {
- const hideButton = post.querySelector(HIDE_BUTTON_SELECTOR);
- if (hideButton) {
- addCustomHideMenuOptions(hideButton); // Attach observer to new hide buttons
- }
- });
- }
- }
- }
- });
- // Also check removed nodes in case backlinks need updating
- mutation.removedNodes.forEach(node => {
- if (node.nodeType === Node.ELEMENT_NODE) {
- if (node.matches(POST_CONTAINER_SELECTOR) || node.querySelector(POST_CONTAINER_SELECTOR)) {
- log(`Mutation: Post removed. Triggering link update.`);
- needsLinkUpdate = true;
- }
- }
- });
- }
- }
- if (needsLinkUpdate) {
- debouncedUpdateAllLinks();
- }
- });
- observer.observe(threadContainer, {
- attributes: true, // Watch for attribute changes (like 'class')
- attributeFilter: ['class'], // Only care about class changes
- attributeOldValue: true,// Need old value to see if 'hidden' changed
- childList: true, // Watch for new nodes being added or removed
- subtree: true // Watch descendants (the posts and their inner content)
- });
- log('MutationObserver attached to thread container for link updates and new menu options.');
- } else {
- warn('Thread container not found. Links and menu options will not update automatically on dynamic changes.');
- }
- })();