// ==UserScript== // @name 8chan Lightweight Extended Suite // @namespace https://greasyfork.org/en/scripts/533173 // @version 2.1.8 // @description Spoiler revealer for 8chan with nested replies // @author impregnator // @match https://8chan.moe/* // @match https://8chan.se/* // @match https://8chan.cc/* // @grant none // @license MIT // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Function to process images and replace spoiler placeholders with thumbnails function processImages(images, isCatalog = false) { images.forEach(img => { // Check if the image is a spoiler placeholder (custom or default) if (img.src.includes('custom.spoiler') || img.src.includes('spoiler.png')) { let fullFileUrl; if (isCatalog) { // Catalog: Get the href from the parent const link = img.closest('a.linkThumb'); if (link) { // Construct the thumbnail URL based on the thread URL fullFileUrl = link.href; const threadMatch = fullFileUrl.match(/\/([a-z0-9]+)\/res\/([0-9]+)\.html$/i); if (threadMatch && threadMatch[1] && threadMatch[2]) { const board = threadMatch[1]; const threadId = threadMatch[2]; // Fetch the thread page to find the actual image URL fetchThreadImage(board, threadId).then(thumbnailUrl => { if (thumbnailUrl) { img.src = thumbnailUrl; } }); } } } else { // Thread: Get the parent element containing the full-sized file URL const link = img.closest('a.imgLink'); if (link) { // Extract the full-sized file URL fullFileUrl = link.href; // Extract the file hash (everything after /.media/ up to the extension) const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i); if (fileHash && fileHash[1]) { // Construct the thumbnail URL using the current domain const thumbnailUrl = `${window.location.origin}/.media/t_${fileHash[1]}`; // Replace the spoiler image with the thumbnail img.src = thumbnailUrl; } } } } }); } // Function to fetch the thread page and extract the thumbnail URL async function fetchThreadImage(board, threadId) { try { const response = await fetch(`https://${window.location.host}/${board}/res/${threadId}.html`); const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, 'text/html'); // Find the first image in the thread's OP post const imgLink = doc.querySelector('.uploadCell a.imgLink'); if (imgLink) { const fullFileUrl = imgLink.href; const fileHash = fullFileUrl.match(/\/\.media\/([a-f0-9]+)\.[a-z0-9]+$/i); if (fileHash && fileHash[1]) { return `${window.location.origin}/.media/t_${fileHash[1]}`; } } return null; } catch (error) { console.error('Error fetching thread image:', error); return null; } } // Process existing images on page load const isCatalogPage = window.location.pathname.includes('catalog.html'); if (isCatalogPage) { const initialCatalogImages = document.querySelectorAll('.catalogCell a.linkThumb img'); processImages(initialCatalogImages, true); } else { const initialThreadImages = document.querySelectorAll('.uploadCell img'); processImages(initialThreadImages, false); } // Set up MutationObserver to handle dynamically added posts const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { // Check each added node for new images mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { if (isCatalogPage) { const newCatalogImages = node.querySelectorAll('.catalogCell a.linkThumb img'); processImages(newCatalogImages, true); } else { const newThreadImages = node.querySelectorAll('.uploadCell img'); processImages(newThreadImages, false); } } }); } }); }); // Observe changes to the document body, including child nodes and subtrees observer.observe(document.body, { childList: true, subtree: true }); })(); //Opening all posts from the catalog in a new tag section // Add click event listener to catalog thumbnail images document.addEventListener('click', function(e) { // Check if the clicked element is an image inside a catalog cell if (e.target.tagName === 'IMG' && e.target.closest('.catalogCell')) { // Find the parent link with class 'linkThumb' const link = e.target.closest('.linkThumb'); if (link) { // Prevent default link behavior e.preventDefault(); // Open the thread in a new tab window.open(link.href, '_blank'); } } }); //Automatically redirect to catalog section // Redirect to catalog if on a board's main page, excluding overboard pages (function() { const currentPath = window.location.pathname; // Check if the path matches a board's main page (e.g., /v/, /a/) but not overboard pages if (currentPath.match(/^\/[a-zA-Z0-9]+\/$/) && !currentPath.match(/^\/(sfw|overboard)\/$/)) { // Redirect to the catalog page window.location.replace(currentPath + 'catalog.html'); } })(); // Text spoiler revealer (function() { // Function to reveal spoilers function revealSpoilers() { const spoilers = document.querySelectorAll('span.spoiler'); spoilers.forEach(spoiler => { // Override default spoiler styles to make text visible spoiler.style.background = 'none'; spoiler.style.color = 'inherit'; spoiler.style.textShadow = 'none'; }); } // Run initially for existing spoilers revealSpoilers(); // Set up MutationObserver to watch for new spoilers const observer = new MutationObserver((mutations) => { mutations.forEach(mutation => { if (mutation.addedNodes.length > 0) { // Check if new nodes contain spoilers mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { const newSpoilers = node.querySelectorAll('span.spoiler'); newSpoilers.forEach(spoiler => { spoiler.style.background = 'none'; spoiler.style.color = 'inherit'; spoiler.style.textShadow = 'none'; }); } }); } }); }); // Observe the document body for changes (new posts) observer.observe(document.body, { childList: true, subtree: true }); })(); //Hash navigation // Add # links to backlinks and quote links for scrolling (function() { // Function to add # link to backlinks and quote links function addHashLinks(container = document) { const links = container.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink'); links.forEach(link => { // Skip if # link already exists or processed if (link.nextSibling && link.nextSibling.classList && link.nextSibling.classList.contains('hash-link-container')) return; if (link.dataset.hashProcessed) return; // Create # link as a span to avoid processing const hashLink = document.createElement('span'); hashLink.textContent = ' #'; hashLink.style.cursor = 'pointer'; hashLink.style.color = '#0000EE'; // Match link color hashLink.title = 'Scroll to post'; hashLink.className = 'hash-link'; hashLink.dataset.hashListener = 'true'; // Mark as processed // Wrap # link in a span to isolate it const container = document.createElement('span'); container.className = 'hash-link-container'; container.appendChild(hashLink); link.insertAdjacentElement('afterend', container); link.dataset.hashProcessed = 'true'; // Mark as processed }); } // Event delegation for hash link clicks to mimic .linkSelf behavior document.addEventListener('click', function(e) { if (e.target.classList.contains('hash-link')) { e.preventDefault(); e.stopPropagation(); const link = e.target.closest('.hash-link-container').previousElementSibling; const postId = link.textContent.replace('>>', ''); if (document.getElementById(postId)) { window.location.hash = `#${postId}`; console.log(`Navigated to post #${postId}`); } else { console.log(`Post ${postId} not found`); } } }, true); // Process existing backlinks and quote links on page load addHashLinks(); console.log('Hash links applied on page load'); // Patch inline reply logic to apply hash links to new inline content if (window.tooltips) { // Patch loadTooltip to apply hash links after content is loaded const originalLoadTooltip = tooltips.loadTooltip; tooltips.loadTooltip = function(element, quoteUrl, sourceId, isInline) { originalLoadTooltip.apply(this, arguments); if (isInline) { // Wait for content to be fully loaded setTimeout(() => { addHashLinks(element); console.log('Hash links applied to loaded tooltip content:', quoteUrl); }, 0); } }; // Patch addLoadedTooltip to ensure hash links are applied const originalAddLoadedTooltip = tooltips.addLoadedTooltip; tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) { originalAddLoadedTooltip.apply(this, arguments); if (isInline) { addHashLinks(htmlContents); console.log('Hash links applied to inline tooltip content:', quoteUrl); } }; // Patch addInlineClick to apply hash links after appending const originalAddInlineClick = tooltips.addInlineClick; tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) { if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) { console.log('Skipped invalid or hash link:', quote.href || quote.textContent); return; } // Clone quote to remove existing listeners const newQuote = quote.cloneNode(true); quote.parentNode.replaceChild(newQuote, quote); quote = newQuote; // Reapply hover events tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId); console.log('Hover events reapplied for:', quoteTarget.quoteUrl); // Add click handler quote.addEventListener('click', function(e) { console.log('linkQuote clicked:', quoteTarget.quoteUrl); if (!tooltips.inlineReplies) { console.log('inlineReplies disabled'); return; } e.preventDefault(); e.stopPropagation(); // Find or create replyPreview let replyPreview = innerPost.querySelector('.replyPreview'); if (!replyPreview) { replyPreview = document.createElement('div'); replyPreview.className = 'replyPreview'; innerPost.appendChild(replyPreview); } // Check for duplicates or loading if (tooltips.loadingPreviews[quoteTarget.quoteUrl] || tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) { console.log('Duplicate or loading:', quoteTarget.quoteUrl); return; } // Create and load inline post const placeHolder = document.createElement('div'); placeHolder.style.whiteSpace = 'normal'; placeHolder.className = 'inlineQuote'; tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true); // Verify post loaded if (!placeHolder.querySelector('.linkSelf')) { console.log('Failed to load post:', quoteTarget.quoteUrl); return; } // Add close button const close = document.createElement('a'); close.innerText = 'X'; close.className = 'closeInline'; close.onclick = () => placeHolder.remove(); placeHolder.querySelector('.postInfo').prepend(close); // Process quotes in the new inline post Array.from(placeHolder.querySelectorAll('.linkQuote')) .forEach(a => tooltips.processQuote(a, false, true)); if (tooltips.bottomBacklinks) { const alts = placeHolder.querySelector('.altBacklinks'); if (alts && alts.firstChild) { Array.from(alts.firstChild.children) .forEach(a => tooltips.processQuote(a, true)); } } // Append to replyPreview and apply hash links replyPreview.appendChild(placeHolder); addHashLinks(placeHolder); console.log('Inline post appended and hash links applied:', quoteTarget.quoteUrl); tooltips.removeIfExists(); }, true); }; // Patch processQuote to skip hash links const originalProcessQuote = tooltips.processQuote; tooltips.processQuote = function(quote, isBacklink) { if (!quote.href || quote.classList.contains('hash-link') || quote.closest('.hash-link-container') || quote.href.includes('#q')) { console.log('Skipped invalid or hash link in processQuote:', quote.href || quote.textContent); return; } originalProcessQuote.apply(this, arguments); }; } // Set up MutationObserver to handle dynamically added or updated backlinks and quote links const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { if (mutation.addedNodes.length) { mutation.addedNodes.forEach(node => { if (node.nodeType === Node.ELEMENT_NODE) { // Check for new backlink or quote link elements const newLinks = node.matches('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink') ? [node] : node.querySelectorAll('.panelBacklinks a, .altBacklinks a, .divMessage .quoteLink'); newLinks.forEach(link => { addHashLinks(link.parentElement); console.log('Hash links applied to new link:', link.textContent); }); } }); } }); }); // Observe changes to the posts container const postsContainer = document.querySelector('.divPosts') || document.body; observer.observe(postsContainer, { childList: true, subtree: true }); })(); //--Hash navigation //Inline reply chains (function() { 'use strict'; console.log('Userscript is running'); // Add CSS for visual nesting const style = document.createElement('style'); style.innerHTML = ` .inlineQuote .replyPreview { margin-left: 20px; border-left: 1px solid #ccc; padding-left: 10px; } .closeInline { color: #ff0000; cursor: pointer; margin-left: 5px; font-weight: bold; } `; document.head.appendChild(style); // Wait for tooltips to initialize window.addEventListener('load', function() { if (!window.tooltips) { console.error('tooltips module not found'); return; } console.log('tooltips module found'); // Ensure Inline Replies is enabled if (!tooltips.inlineReplies) { console.log('Enabling Inline Replies'); localStorage.setItem('inlineReplies', 'true'); tooltips.inlineReplies = true; // Check and update the checkbox, retrying if not yet loaded const enableCheckbox = () => { const inlineCheckbox = document.getElementById('settings-SW5saW5lIFJlcGxpZX'); if (inlineCheckbox) { inlineCheckbox.checked = true; console.log('Inline Replies checkbox checked'); return true; } console.warn('Inline Replies checkbox not found, retrying...'); return false; }; // Try immediately if (!enableCheckbox()) { // Retry every 500ms up to 5 seconds let attempts = 0; const maxAttempts = 10; const interval = setInterval(() => { if (enableCheckbox() || attempts >= maxAttempts) { clearInterval(interval); if (attempts >= maxAttempts) { console.error('Failed to find Inline Replies checkbox after retries'); } } attempts++; }, 500); } } else { console.log('Inline Replies already enabled'); } // Override addLoadedTooltip to ensure replyPreview exists const originalAddLoadedTooltip = tooltips.addLoadedTooltip; tooltips.addLoadedTooltip = function(htmlContents, tooltip, quoteUrl, replyId, isInline) { console.log('addLoadedTooltip called for:', quoteUrl); originalAddLoadedTooltip.apply(this, arguments); if (isInline) { let replyPreview = htmlContents.querySelector('.replyPreview'); if (!replyPreview) { replyPreview = document.createElement('div'); replyPreview.className = 'replyPreview'; htmlContents.appendChild(replyPreview); } } }; // Override addInlineClick for nested replies, excluding post number links tooltips.addInlineClick = function(quote, innerPost, isBacklink, quoteTarget, sourceId) { // Skip post number links (href starts with #q) if (quote.href.includes('#q')) { console.log('Skipping post number link:', quote.href); return; } // Remove existing listeners by cloning const newQuote = quote.cloneNode(true); quote.parentNode.replaceChild(newQuote, quote); quote = newQuote; // Reapply hover events to preserve preview functionality tooltips.addHoverEvents(quote, innerPost, quoteTarget, sourceId); console.log('Hover events reapplied for:', quoteTarget.quoteUrl); // Add click handler quote.addEventListener('click', function(e) { console.log('linkQuote clicked:', quoteTarget.quoteUrl); if (!tooltips.inlineReplies) { console.log('inlineReplies disabled'); return; } e.preventDefault(); e.stopPropagation(); // Prevent site handlers // Find or create replyPreview let replyPreview = innerPost.querySelector('.replyPreview'); if (!replyPreview) { replyPreview = document.createElement('div'); replyPreview.className = 'replyPreview'; innerPost.appendChild(replyPreview); } // Check for duplicates or loading if (tooltips.loadingPreviews[quoteTarget.quoteUrl] || tooltips.quoteAlreadyAdded(quoteTarget.quoteUrl, innerPost)) { console.log('Duplicate or loading:', quoteTarget.quoteUrl); return; } // Create and load inline post const placeHolder = document.createElement('div'); placeHolder.style.whiteSpace = 'normal'; placeHolder.className = 'inlineQuote'; tooltips.loadTooltip(placeHolder, quoteTarget.quoteUrl, sourceId, true); // Verify post loaded if (!placeHolder.querySelector('.linkSelf')) { console.log('Failed to load post:', quoteTarget.quoteUrl); return; } // Add close button const close = document.createElement('a'); close.innerText = 'X'; close.className = 'closeInline'; close.onclick = () => placeHolder.remove(); placeHolder.querySelector('.postInfo').prepend(close); // Process quotes in the new inline post Array.from(placeHolder.querySelectorAll('.linkQuote')) .forEach(a => tooltips.processQuote(a, false, true)); if (tooltips.bottomBacklinks) { const alts = placeHolder.querySelector('.altBacklinks'); if (alts && alts.firstChild) { Array.from(alts.firstChild.children) .forEach(a => tooltips.processQuote(a, true)); } } // Append to replyPreview replyPreview.appendChild(placeHolder); console.log('Inline post appended:', quoteTarget.quoteUrl); tooltips.removeIfExists(); }, true); // Use capture phase }; // Reprocess all existing linkQuote and backlink elements, excluding post numbers console.log('Reprocessing linkQuote elements'); const quotes = document.querySelectorAll('.linkQuote, .panelBacklinks a'); quotes.forEach(quote => { const innerPost = quote.closest('.innerPost, .innerOP'); if (!innerPost) { console.log('No innerPost found for quote:', quote.href); return; } // Skip post number links if (quote.href.includes('#q')) { console.log('Skipping post number link:', quote.href); return; } const isBacklink = quote.parentElement.classList.contains('panelBacklinks') || quote.parentElement.classList.contains('altBacklinks'); const quoteTarget = api.parsePostLink(quote.href); const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post; tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId); }); // Observe for dynamically added posts const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType !== 1) return; const newQuotes = node.querySelectorAll('.linkQuote, .panelBacklinks a'); newQuotes.forEach(quote => { if (quote.dataset.processed || quote.href.includes('#q')) { if (quote.href.includes('#q')) { console.log('Skipping post number link:', quote.href); } return; } quote.dataset.processed = 'true'; const innerPost = quote.closest('.innerPost, .innerOP'); if (!innerPost) return; const isBacklink = quote.parentElement.classList.contains('panelBacklinks') || quote.parentElement.classList.contains('altBacklinks'); const quoteTarget = api.parsePostLink(quote.href); const sourceId = api.parsePostLink(innerPost.querySelector('.linkSelf').href).post; tooltips.addInlineClick(quote, innerPost, isBacklink, quoteTarget, sourceId); }); }); }); }); observer.observe(document.querySelector('.divPosts') || document.body, { childList: true, subtree: true }); console.log('MutationObserver set up'); }); })(); //--Inline replies //Auto TOS accept (function() { 'use strict'; // Check if on the disclaimer page if (window.location.pathname === '/.static/pages/disclaimer.html') { // Redirect to confirmed page window.location.replace('https://8chan.se/.static/pages/confirmed.html'); console.log('Automatically redirected from disclaimer to confirmed page'); } })(); //--Auto TOS accept