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.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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();
        }
    });

})();