您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Spoiler revealer for 8chan with nested replies
当前为
- // ==UserScript==
- // @name 8chan Lightweight Extended Suite
- // @namespace https://greasyfork.org/en/scripts/533173
- // @version 2.1.1
- // @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
- // ==/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 <a class="linkThumb">
- 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 <a> 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
- });
- })();
- //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');
- });
- })();