IPTorrents Thread Image Preview

Preview images from IPTorrents thread on hover

// ==UserScript==
// @name         IPTorrents Thread Image Preview
// @namespace    http://tampermonkey.net/
// @version      1.5
// @description  Preview images from IPTorrents thread on hover
// @author       SH3LL
// @match        https://iptorrents.com/t*
// @grant        none
// ==/UserScript==
(function() {
    'use strict';

    // Create tooltip container
    const tooltip = document.createElement('div');
    Object.assign(tooltip.style, {
        position: 'absolute',
        padding: '4px',
        background: '#111',
        border: '1px solid #444',
        borderRadius: '3px',
        boxShadow: '0 0 6px rgba(0,0,0,0.7)',
        zIndex: 9999,
        display: 'none',
        width: 'fit-content', // Shrink to fit the content's natural width
        height: 'fit-content' // Shrink to fit the content's natural height
    });
    document.body.appendChild(tooltip);

    // Cache to avoid repeated fetches
    const cache = {};

    // Extract all images from thread HTML
    function extractImage(htmlText) {
        const tmp = document.createElement('div');
        tmp.innerHTML = htmlText;
        const imageSources = [];

        // Try original selector
        let images = tmp.querySelectorAll('blockquote .zoomWarp img');
        images.forEach(img => {
            if (img.src) {
                imageSources.push(img.src);
            }
        });

        // Try alternative selector for lazy-loading
        images = tmp.querySelectorAll('blockquote .zoomWarp img[data-src]');
        images.forEach(img => {
            if (img.dataset.src) {
                imageSources.push(img.dataset.src);
            }
        });

        // Try more generic selector
        images = tmp.querySelectorAll('blockquote img');
        images.forEach(img => {
            if (img.src || img.dataset.src) {
                imageSources.push(img.src || img.dataset.src);
            }
        });

        // Remove duplicates while preserving order
        const uniqueSources = [...new Set(imageSources)];

        return uniqueSources.length > 0 ? uniqueSources : null;
    }

    // Show tooltip with all images
    function showTooltip(x, y, content) {
        tooltip.innerHTML = '';
        if (Array.isArray(content)) {
            const container = document.createElement('div');
            container.style.display = 'flex';
            container.style.flexWrap = 'wrap';
            container.style.gap = '4px';
            container.style.width = 'fit-content'; // Ensure container shrinks to wrapped content

            // Set max-width to accommodate exactly two images per row (400px each + 4px gap)
            const imagesPerRow = 2;
            const imageWidth = 400;
            const gap = 4;
            const constrainedWidth = imagesPerRow * (imageWidth + gap) - gap; // Subtract gap for last image in row
            container.style.maxWidth = `${constrainedWidth}px`; // Limit container width to two images

            content.forEach((src, index) => {
                const img = document.createElement('img');
                img.src = src;
                img.style.maxWidth = `${imageWidth}px`;
                img.style.maxHeight = '400px';
                img.style.display = 'block'; // Prevent inline spacing issues
                img.style.flex = `0 0 calc(50% - ${gap / 2}px)`; // Each image takes half the container width minus half the gap
                img.onerror = () => { img.alt = 'Failed to load image'; };
                container.appendChild(img);

                // Add line break after every second image
                if ((index + 1) % imagesPerRow === 0 && index < content.length - 1) {
                    const lineBreak = document.createElement('div');
                    lineBreak.style.width = '100%'; // Force line break
                    container.appendChild(lineBreak);
                }
            });
            tooltip.appendChild(container);

            // Force reflow to ensure correct width after wrapping
            tooltip.style.display = 'inline-block';
            container.offsetWidth; // Trigger reflow
        } else {
            tooltip.innerHTML = content;
        }

        // Measure tooltip size after content is added
        tooltip.style.display = 'inline-block';
        const tooltipWidth = tooltip.offsetWidth;
        const tooltipHeight = tooltip.offsetHeight;

        // Adjust position to prevent overflow
        const viewportWidth = window.innerWidth;
        const viewportHeight = window.innerHeight;
        let adjustedX = x + 12;
        let adjustedY = y + 12;

        // If tooltip would overflow right edge, move it left
        if (adjustedX + tooltipWidth > viewportWidth - 10) {
            adjustedX = x - tooltipWidth - 12;
        }
        // If tooltip would overflow bottom edge, move it up
        if (adjustedY + tooltipHeight > viewportHeight - 10) {
            adjustedY = y - tooltipHeight - 12;
        }

        // Ensure tooltip doesn't go off-screen on left or top
        adjustedX = Math.max(10, adjustedX);
        adjustedY = Math.max(10, adjustedY);

        tooltip.style.left = adjustedX + 'px';
        tooltip.style.top = adjustedY + 'px';
    }

    // Hide tooltip
    function hideTooltip() {
        tooltip.style.display = 'none';
        tooltip.innerHTML = '';
    }

    // Handle mouseover and mousemove events
    async function onLinkHover(e) {
        const a = e.currentTarget;
        const threadUrl = a.href;
        const mx = e.pageX, my = e.pageY;

        // Check cache
        if (cache.hasOwnProperty(threadUrl)) {
            console.log('Loaded from cache:', threadUrl);
            const imgSources = cache[threadUrl];
            if (imgSources) {
                showTooltip(mx, my, imgSources);
            } else {
                showTooltip(mx, my, 'No preview available');
            }
            return;
        }

        // Mark as in progress
        cache[threadUrl] = null;

        try {
            console.log('Fetching:', threadUrl);
            const res = await fetch(threadUrl, {
                credentials: 'include',
                headers: {
                    'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8'
                }
            });
            if (!res.ok) {
                throw new Error(`HTTP error! status: ${res.status}`);
            }
            const text = await res.text();
            const imgSources = extractImage(text);
            cache[threadUrl] = imgSources;

            if (imgSources) {
                showTooltip(mx, my, imgSources);
            } else {
                showTooltip(mx, my, 'No preview available');
            }
        } catch (err) {
            cache[threadUrl] = null;
            showTooltip(mx, my, 'Error loading preview');
        }
    }

    // Handle mouseout event
    function onLinkOut() {
        hideTooltip();
    }

    // Attach events to thread links
    function attachToLinks() {
        const links = document.querySelectorAll('td.al a[href^="/t/"]');
        links.forEach(a => {
            if (!a.dataset.previewBound) {
                a.addEventListener('mouseover', onLinkHover);
                a.addEventListener('mousemove', onLinkHover);
                a.addEventListener('mouseout', onLinkOut);
                a.dataset.previewBound = '1';
            }
        });
    }

    // Run on load
    attachToLinks();

    // Observe for dynamic content
    const obs = new MutationObserver(attachToLinks);
    obs.observe(document.body, { childList: true, subtree: true });
})();