您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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);
- })();