Nexus Mods Gallery Preview Hover (Universal)

Show thumbgallery preview images when hovering on a NexusMods main mod link or mod thumbnail, works for any game. Preview popup is fixed near the anchor, mouse can interact with popup.

Verze ze dne 06. 06. 2025. Zobrazit nejnovější verzi.

// ==UserScript==
// @name         Nexus Mods Gallery Preview Hover (Universal)
// @namespace    http://tampermonkey.net/
// @version      0.5
// @description  Show thumbgallery preview images when hovering on a NexusMods main mod link or mod thumbnail, works for any game. Preview popup is fixed near the anchor, mouse can interact with popup.
// @author       GPT
// @license      MIT
// @match        *://*/*
// @grant        GM_xmlhttpRequest
// @connect      nexusmods.com
// ==/UserScript==

(function() {
    'use strict';

    let previewDiv = null;
    let previewTimer = null;
    let currentLink = null;
    let hoverOnPreview = false;
    let hoverOnLink = false;

    // Only match main mod links (mods/<number>), for any game on NexusMods
    function isModMainLink(href) {
        // e.g. https://www.nexusmods.com/skyrimspecialedition/mods/12345
        //      https://www.nexusmods.com/fallout4/mods/6789
        return /^https?:\/\/(www\.)?nexusmods\.com\/[^\/]+\/mods\/\d+$/.test(href);
    }

    // Create the preview popup near the anchor (link or image)
    function createPreview(images, anchor) {
        removePreview();
        previewDiv = document.createElement('div');
        previewDiv.style.position = 'absolute';
        previewDiv.style.zIndex = 99999;
        previewDiv.style.background = '#222';
        previewDiv.style.padding = '8px';
        previewDiv.style.borderRadius = '8px';
        previewDiv.style.boxShadow = '0 2px 12px rgba(0,0,0,0.35)';
        previewDiv.style.maxWidth = '560px';
        previewDiv.style.maxHeight = '420px';
        previewDiv.style.overflowY = 'auto';
        previewDiv.style.overflowX = 'hidden';
        previewDiv.style.display = 'grid';
        previewDiv.style.gridTemplateColumns = '1fr 1fr';
        previewDiv.style.gap = '8px';

        // Add all images to the popup grid
        for (let img of images) {
            let i = document.createElement('img');
            i.src = img;
            i.style.maxHeight = '180px';
            i.style.maxWidth = '260px';
            i.style.width = '100%';
            i.style.objectFit = 'cover';
            i.style.borderRadius = '4px';
            previewDiv.appendChild(i);
        }

        document.body.appendChild(previewDiv);

        // Position the preview below and slightly to the right of the anchor
        let rect = anchor.getBoundingClientRect();
        let scrollX = window.scrollX || document.documentElement.scrollLeft;
        let scrollY = window.scrollY || document.documentElement.scrollTop;
        let left = rect.left + scrollX + 8;
        let top = rect.bottom + scrollY + 6;
        previewDiv.style.left = left + 'px';
        previewDiv.style.top = top + 'px';

        // When mouse enters/leaves the popup
        previewDiv.addEventListener('mouseenter', function() {
            hoverOnPreview = true;
            clearTimeout(previewTimer);
        });
        previewDiv.addEventListener('mouseleave', function() {
            hoverOnPreview = false;
            startPreviewTimeout();
        });
    }

    // Start delayed removal of preview
    function startPreviewTimeout() {
        previewTimer = setTimeout(() => { removePreview(); }, 200);
    }

    // Remove the preview popup
    function removePreview() {
        if (previewDiv && previewDiv.parentNode) {
            previewDiv.parentNode.removeChild(previewDiv);
            previewDiv = null;
        }
        currentLink = null;
        clearTimeout(previewTimer);
        hoverOnPreview = false;
        hoverOnLink = false;
    }

    // Fetch thumbgallery images from the mod page
    function fetchGallery(link, anchor) {
        if (currentLink === link) return; // Prevent duplicate requests
        currentLink = link;
        GM_xmlhttpRequest({
            method: 'GET',
            url: link,
            onload: function(response) {
                let html = response.responseText;
                let match = html.match(/<ul class="thumbgallery gallery clearfix"[^>]*>([\s\S]*?)<\/ul>/);
                if (match) {
                    let ul = match[1];
                    let imgRegex = /<img\s+[^>]*src="([^"]+)"[^>]*>/g;
                    let images = [];
                    let m;
                    while ((m = imgRegex.exec(ul)) !== null) {
                        images.push(m[1]);
                    }
                    if (images.length > 0) {
                        createPreview(images, anchor);
                    }
                }
            }
        });
    }

    // Listen for mouseover on mod main links
    document.body.addEventListener('mouseover', function(e) {
        let target = e.target;
        if (target.tagName === 'A' && isModMainLink(target.href)) {
            hoverOnLink = true;
            fetchGallery(target.href, target);
            target.addEventListener('mouseleave', function handler() {
                hoverOnLink = false;
                startPreviewTimeout();
                target.removeEventListener('mouseleave', handler);
            });
        }
    }, true);

    // Listen for mouseover on mod thumbnails (img inside mod main link)
    document.body.addEventListener('mouseover', function(e) {
        let target = e.target;
        // Typical thumbnails are <img> inside <a>
        if (
            target.tagName === 'IMG'
            && target.closest('a')
            && isModMainLink(target.closest('a').href)
        ) {
            let a = target.closest('a');
            hoverOnLink = true;
            fetchGallery(a.href, target);
            a.addEventListener('mouseleave', function handler() {
                hoverOnLink = false;
                startPreviewTimeout();
                a.removeEventListener('mouseleave', handler);
            });
        }
    }, true);

    // Remove preview when clicking elsewhere on the page
    document.addEventListener('mousedown', function(e) {
        if (previewDiv && !previewDiv.contains(e.target)) {
            removePreview();
        }
    });

})();
长期地址
遇到问题?请前往 GitHub 提 Issues。