Shoutbox Direct Image Link Viewer

Converts image links (jpg/png/webp) in shoutbox to inline thumbnails with click-to-enlarge fullscreen popup.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Shoutbox Direct Image Link Viewer
// @icon         https://icons.duckduckgo.com/ip3/torrentbd.net.ico
// @namespace    foxbinner
// @version      1.0.1
// @description  Converts image links (jpg/png/webp) in shoutbox to inline thumbnails with click-to-enlarge fullscreen popup.
// @match        https://*.torrentbd.com/*
// @match        https://*.torrentbd.net/*
// @match        https://*.torrentbd.org/*
// @match        https://*.torrentbd.me/*
// @grant        none
// @author       foxbinner
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // Ignore Links - Tenor.com links should remain as regular links
    const ignoreRegex = /https?:\/\/(?:tenor\.com)/i;

    // Fixed regex - handles GIFs with query params (?abc=123)
    const imageRegex = /https?:\/\/[^\s'"><]+\.(?:png|jpe?g|webp|gif)(?:\?[^\s'">]*)?(?=[^\w\-]|$)/gi;

    function convertLinksInNode(node) {
        const textField = node.querySelector('.shout-text');
        if (!textField) return;

        const walker = document.createTreeWalker(textField, NodeFilter.SHOW_TEXT, null);
        const textNodes = [];
        let curr;
        while ((curr = walker.nextNode())) textNodes.push(curr);

        textNodes.forEach(textNode => {
            const text = textNode.nodeValue;
            if (!imageRegex.test(text)) return;

            const frag = document.createDocumentFragment();
            let lastIndex = 0;
            imageRegex.lastIndex = 0;

            let match;
            while ((match = imageRegex.exec(text)) !== null) {
                const url = match[0];

                // Skip if matches ignore pattern (Tenor links)
                if (ignoreRegex.test(url)) {
                    if (match.index > lastIndex) {
                        frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
                    }
                    frag.appendChild(document.createTextNode(url));
                    frag.appendChild(document.createTextNode(' '));
                    lastIndex = imageRegex.lastIndex;
                    continue;
                }

                if (match.index > lastIndex) {
                    frag.appendChild(document.createTextNode(text.slice(lastIndex, match.index)));
                }

                const img = document.createElement('img');
                img.src = url;
                img.dataset.fullsrc = url;
                img.style.cssText = 'max-width:100px; max-height:100px; vertical-align:middle; display:inline; margin:0 4px; cursor:pointer; border-radius:4px;';
                img.alt = 'image';

                frag.appendChild(document.createTextNode(' '));
                frag.appendChild(img);
                frag.appendChild(document.createTextNode(' '));

                lastIndex = imageRegex.lastIndex;
            }

            if (lastIndex < text.length) {
                frag.appendChild(document.createTextNode(text.slice(lastIndex)));
            }

            textNode.parentNode.replaceChild(frag, textNode);
        });
    }

    function createOverlay(img) {
        const existing = document.querySelector('.image-overlay');
        if (existing) existing.remove();

        const overlay = document.createElement('div');
        overlay.className = 'image-overlay';
        overlay.style.cssText = `
            position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
            background: rgba(0,0,0,0.9); z-index: 9999;
            display: flex; align-items: center; justify-content: center;
            cursor: pointer;
        `;

        const fullImg = document.createElement('img');
        fullImg.src = img.dataset.fullsrc || img.src;
        fullImg.style.cssText = `
            max-width: 90vw; max-height: 90vh;
            border-radius: 8px; box-shadow: 0 10px 30px rgba(0,0,0,0.5);
        `;

        overlay.appendChild(fullImg);
        overlay.onclick = () => overlay.remove();
        fullImg.onclick = (e) => e.stopPropagation();

        document.addEventListener('keydown', function escClose(e) {
            if (e.key === 'Escape') overlay.remove();
        }, { once: true });

        document.body.appendChild(overlay);
    }

    function addClickHandlers() {
        document.querySelectorAll('.shout-text img[data-fullsrc]').forEach(img => {
            img.onclick = (e) => {
                e.preventDefault();
                e.stopPropagation();
                createOverlay(img);
            };
            if (img.parentElement.tagName === 'A') {
                img.parentElement.onclick = (e) => e.preventDefault();
            }
        });
    }

    function scanAllShouts() {
        document.querySelectorAll('.shout-item').forEach(convertLinksInNode);
        addClickHandlers();
    }

    scanAllShouts();

    const shoutContainer = document.querySelector('#shouts-container');
    if (shoutContainer) {
        const observer = new MutationObserver((mutations) => {
            mutations.forEach((m) => {
                m.addedNodes.forEach((node) => {
                    if (node.nodeType === Node.ELEMENT_NODE &&
                        (node.classList?.contains('shout-item') ||
                         node.querySelector?.('.shout-item'))) {
                        const shoutItem = node.classList?.contains('shout-item') ? node : node.querySelector('.shout-item');
                        setTimeout(() => {
                            convertLinksInNode(shoutItem);
                            addClickHandlers();
                        }, 50);
                    }
                });
            });
        });
        observer.observe(shoutContainer, { childList: true, subtree: true });
    }
})();