Greasy Fork is available in English.
Pre-sizes spoiler images to thumbnail dimensions and shows thumbnail on hover on 8chan.
当前为
// ==UserScript==
// @name 8chan Unspoiler Thumbnails on Mouse Hover
// @namespace sneed
// @version 0.5.1
// @description Pre-sizes spoiler images to thumbnail dimensions and shows thumbnail on hover on 8chan.
// @author Gemini 2.5
// @license MIT
// @match https://8chan.moe/*/res/*.html*
// @match https://8chan.se/*/res/*.html*
// ==/UserScript==
(function() {
'use strict';
// Define the selector for both types of spoiler images
// This matches the old /spoiler.png OR any image whose src ends with /custom.spoiler
const spoilerImgSelector = 'img[src="/spoiler.png"], img[src$="/custom.spoiler"]';
// Function to extract the hash from a FoolFuuka image URL
// Expects URL like /path/to/media/HASH.EXT
function getHashFromImageUrl(imageUrl) {
if (!imageUrl) return null;
const parts = imageUrl.split('/');
const filename = parts.pop(); // Get HASH.EXT
if (!filename) return null;
const hash = filename.split('.')[0]; // Get HASH (assuming no dots in hash)
return hash || null;
}
// Function to construct the thumbnail URL
// Assumes thumbnail is in the same directory as the full image with 't_' prefix and no extension
function getThumbnailUrl(fullImageUrl, hash) {
if (!fullImageUrl || !hash) return null;
const parts = fullImageUrl.split('/');
parts.pop(); // Remove the filename (HASH.EXT)
const basePath = parts.join('/') + '/'; // Rejoin path parts and add trailing slash
return basePath + 't_' + hash; // Use the t_HASH format
}
// --- Dimension Setting Logic ---
// Function to load the thumbnail invisibly and set the spoiler image's dimensions
function setSpoilerDimensionsFromThumbnail(imgLink) {
// Find the specific spoiler image within this link using the updated selector
const spoilerImg = imgLink.querySelector(spoilerImgSelector);
// Only proceed if we have a spoiler image and its dimensions haven't already been set by this script
// We use a data attribute on the spoiler image itself to track if dimensions were attempted.
if (!spoilerImg || spoilerImg.dataset.dimensionsSet) {
return;
}
const fullImageUrl = imgLink.href;
const hash = getHashFromImageUrl(fullImageUrl);
if (!hash) return;
const thumbnailUrl = getThumbnailUrl(fullImageUrl, hash);
if (!thumbnailUrl) {
// Mark as dimensions attempted, but failed due to URL issues
spoilerImg.dataset.dimensionsSet = 'failed_url';
return;
}
// Create a temporary image element to load the thumbnail and get its dimensions
const tempImg = new Image(); // Use new Image() which is efficient for this
tempImg.style.display = 'none'; // Hide it
tempImg.style.position = 'absolute'; // Position it off-screen or just hidden
tempImg.style.left = '-9999px';
tempImg.style.top = '-9999px';
// Mark the spoiler image now as dimensions are being attempted.
spoilerImg.dataset.dimensionsSet = 'attempting';
tempImg.addEventListener('load', function() {
// Set the dimensions of the spoiler image if loaded successfully
if (this.naturalWidth > 0 && this.naturalHeight > 0) {
spoilerImg.width = this.naturalWidth;
spoilerImg.height = this.naturalHeight;
// Mark as dimensions successfully set
spoilerImg.dataset.dimensionsSet = 'success';
} else {
// Mark as failed if dimensions were unexpectedly zero
spoilerImg.dataset.dimensionsSet = 'failed_zero_dim';
console.warn(`[SpoilerThumbnailHover] Thumbnail loaded but reported zero dimensions: ${thumbnailUrl}`);
}
// Clean up the temporary image regardless of success/failure after load
if (this.parentNode) {
this.parentNode.removeChild(this);
}
});
tempImg.addEventListener('error', function() {
console.warn(`[SpoilerThumbnailHover] Failed to load thumbnail: ${thumbnailUrl}`);
// Mark as failed on error
spoilerImg.dataset.dimensionsSet = 'failed_error';
// Clean up the temporary image
if (this.parentNode) {
this.parentNode.removeChild(this);
}
// Don't set dimensions if load failed. The spoiler keeps its default size.
});
// Append the temporary image to the body to start loading
// This must happen *after* setting up event listeners.
document.body.appendChild(tempImg);
// Start loading the image by setting the src
tempImg.src = thumbnailUrl;
}
// --- Hover Event Handlers ---
function handleLinkMouseEnter() {
const imgLink = this; // The .imgLink element
// Find the specific spoiler image within this link using the updated selector
const spoilerImg = imgLink.querySelector(spoilerImgSelector);
const existingHoverThumbnail = imgLink.querySelector('img.hoverThumbnail');
// Only proceed if there's a visible spoiler image (of either type) and no hover thumbnail already exists
if (!spoilerImg || spoilerImg.style.display === 'none' || existingHoverThumbnail) {
return;
}
// Ensure dimensions were at least attempted before proceeding with hover effect
// if (spoilerImg.dataset.dimensionsSet === 'attempting') {
// // Thumbnail loading is still in progress, maybe wait or do nothing?
// // Doing nothing for now is simplest. The spoiler stays default size until load finishes.
// return;
// }
// Removed the check above because we want the hover to work even if dimensions failed or are pending.
// The thumbnail creation below will just use the *current* size of the spoiler img.
const fullImageUrl = imgLink.href; // Use href of the imgLink for the full image URL
const hash = getHashFromImageUrl(fullImageUrl);
if (!hash) return;
const thumbnailUrl = getThumbnailUrl(fullImageUrl, hash);
if (!thumbnailUrl) return;
// Create the thumbnail image element
const hoverThumbnail = document.createElement('img');
hoverThumbnail.src = thumbnailUrl;
hoverThumbnail.classList.add('hoverThumbnail'); // Add a class to identify our element
// Set thumbnail dimensions to match the current spoiler image size.
// The spoiler image should now have the correct size set by setSpoilerDimensionsFromThumbnail.
// If dimension setting failed or hasn't completed, it will use the default spoiler size.
if (spoilerImg.width > 0 && spoilerImg.height > 0) {
hoverThumbnail.width = spoilerImg.width;
hoverThumbnail.height = spoilerImg.height;
}
// Note: If the thumbnail loads *after* the mouse enters but before mouse leaves,
// the dimensions might be updated on the spoiler, but the hover thumbnail already created
// won't update dynamically. This is an acceptable minor edge case.
// Insert the thumbnail right before the spoiler image
imgLink.insertBefore(hoverThumbnail, spoilerImg);
// Hide the original spoiler image
spoilerImg.style.display = 'none';
}
function handleLinkMouseLeave() {
const imgLink = this; // The .imgLink element
// Find the specific spoiler image within this link using the updated selector
const spoilerImg = imgLink.querySelector(spoilerImgSelector);
const hoverThumbnail = imgLink.querySelector('img.hoverThumbnail');
// If our hover thumbnail exists, remove it
if (hoverThumbnail) {
hoverThumbnail.remove();
}
// Check if the board's full image expansion is visible
// Selects any img within the link that is NOT a spoiler image type, and check if it's visible.
const otherImages = imgLink.querySelectorAll(`img:not(${spoilerImgSelector})`);
let isOtherImageVisible = false;
for(const img of otherImages) {
// Check if the image is not hidden by display: none or visibility: hidden etc.
// offsetParent is null if the element or its parent is display: none
// Checking style.display is more direct for FoolFuuka's toggle
if (img.style.display !== 'none') {
isOtherImageVisible = true;
break;
}
}
// Show the original spoiler image again IF
// 1. It exists and is still one of the spoiler image types
// 2. It's currently hidden (style.display === 'none') - implies our script or board script hid it
// 3. The board's expanded image is NOT currently visible.
// 4. Add a check to ensure the spoiler image is still in the DOM hierarchy of the link.
if (spoilerImg && imgLink.contains(spoilerImg) && spoilerImg.matches(spoilerImgSelector) && spoilerImg.style.display === 'none' && !isOtherImageVisible) {
spoilerImg.style.display = ''; // Reset to default display
}
}
// Function to process an individual imgLink element
function processImgLink(imgLink) {
// Prevent processing multiple times
if (imgLink.dataset.spoilerHoverProcessed) {
return;
}
// Find the specific spoiler image within this link using the updated selector
const spoilerImg = imgLink.querySelector(spoilerImgSelector);
// Only process if this link contains a spoiler image (of either type)
if (!spoilerImg) {
return;
}
imgLink.dataset.spoilerHoverProcessed = 'true'; // Mark element as processed
// 1. Attempt to set spoiler dimensions based on thumbnail as soon as possible
// This happens asynchronously via the temp image loader.
setSpoilerDimensionsFromThumbnail(imgLink);
// 2. Attach the hover listeners for showing the thumbnail on hover
// These listeners rely on the spoiler image potentially having updated dimensions
// by the time the mouse enters.
imgLink.addEventListener('mouseenter', handleLinkMouseEnter);
imgLink.addEventListener('mouseleave', handleLinkMouseLeave);
// Optional: Handle clicks on the link to ensure the hover thumbnail is removed
// immediately if the user clicks to expand the image.
// However, the handleLinkMouseLeave check for isOtherImageVisible should handle this
// when the mouse leaves after clicking/expanding. Let's stick to just mouse events for now.
}
// Function to find all imgLink elements within a container and process them
function processContainer(container) {
// Select imgLink elements
const imgLinks = container.querySelectorAll('.imgLink');
imgLinks.forEach(processImgLink); // Process each found imgLink
}
// Use a MutationObserver to handle new nodes being added to the DOM (e.g., infinite scroll)
const observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes && mutation.addedNodes.length > 0) {
mutation.addedNodes.forEach(function(node) {
// nodeType 1 is Element
if (node.nodeType === Node.ELEMENT_NODE) {
// If the added node is an imgLink (potentially with a spoiler)
// Or if it's a container that might contain imgLinks (like posts, board content)
if (node.matches('.imgLink')) {
processImgLink(node); // Process just this specific link
} else {
// Select all imgLink elements within the added node's subtree
processContainer(node);
}
}
});
}
});
});
// Configuration for the observer:
// - childList: true means observe direct children being added/removed
// - subtree: true means observe changes in the entire subtree
observer.observe(document.body, { childList: true, subtree: true });
// Process imgLink elements that are already present in the DOM when the script runs
processContainer(document);
})();