SubDL Image Preview

Display image previews on subdl.com .

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         SubDL Image Preview
// @namespace    http://tampermonkey.net/
// @version      2.2
// @description  Display image previews on subdl.com .
// @author       dr.bobo0
// @match        https://subdl.com/u/*
// @icon         data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB8AAAAgCAMAAADdXFNzAAAAXVBMVEVHcEz/7ir/7ir/7ir/7ir/7ir/7ir/7in/7ir/7ir/7ir/7ir/7Sr/7ir/7ir/7ir/9Sn/8iopKjMxMTMdIDMKEDSDfDBkXzH/+inBtS3bzSzSxC1GRDLs3SuZkC8BXe1rAAAAD3RSTlMAmpG5qyvRFvBGY9wGycP/EwDKAAABG0lEQVQokX2Ti5KDIAxFUSmgtg1P6/v/P3NFB0y03TvjjPFAIjeEsSzZ8FaBankj2V21gFOivtDnG6jeT4wfcNcD5f6CAer/duMMPzDAgXmOtYnSJsWcZl982DXmBbFClXd31lm7PR+dPlW4euRd12G+/UGDue11PwfEG1Zg7hdjpumsDwUTmLvZwHaC84SCqfxuxrDV7okDCpujh+D8ShcQ8/rVuzBowhWO9Me6sKD6ir1IOpidcyh8sZJgs3jiT4l7r7UGM3nMa+qv1aafqb9neyMfpxHz2GCJuPPeO3fy/aKnDpgh+KiwJl4cFyhZYKZhV8IqDU4+3SGS/fcFRgMg1Y0qOoTFBRfX+ZQcUf5tglldVqIVVYmH9w/WzDC9Fj6LqQAAAABJRU5ErkJggg==
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    console.log("UserScript started.");

    const storagePrefix = "subdl_image_cache_";
    const maxCacheAge = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds

    const exclusionList = [
        '/', // Home
        '/panel',
        '/panel/my-subtitles',
        '/panel/account',
        '/panel/api',
        '/latest',
        '/popular',
        'https://t.me/subdl_com', // External link
        '/ads', // Advertise link
        '/api-doc', // API documentation
        '/panel/logout',
        '/login', // Login page
        '#', // Placeholder links like Privacy Policy
        '/signup' // Signup page
    ];

    function clearOldCache() {
        console.log("Clearing old cache entries.");
        const now = Date.now();
        for (let i = 0; i < localStorage.length; i++) {
            const key = localStorage.key(i);
            if (key && key.startsWith(storagePrefix)) {
                try {
                    const item = JSON.parse(localStorage.getItem(key));
                    if (now - item.timestamp > maxCacheAge) {
                        localStorage.removeItem(key);
                        console.log(`Removed old cache entry: ${key}`);
                    }
                } catch (e) {
                    console.error(`Error parsing cache item: ${key}`, e);
                    localStorage.removeItem(key);
                }
            }
        }
    }

    function addPreviewToLinks() {
        console.log("Adding preview to links.");
        const links = document.querySelectorAll('a[href*="/s/info/"]');
        let linksFound = 0;

        links.forEach(link => {
            if (shouldAddPreview(link)) {
                linksFound++;
                if (!link.dataset.hasEventListener) {
                    console.log("Adding event listener to link:", link.href);
                    link.dataset.hasEventListener = 'true';

                    link.addEventListener("mouseover", function() {
                        console.log("Mouseover event triggered for link:", this.href);
                        let previewContainer = createPreviewContainer();

                        document.body.appendChild(previewContainer);
                        showLoadingSpinner(previewContainer);
                        fetchImage(this.href, previewContainer);

                        let removeMousemoveListener = addMousemoveListener(previewContainer);
                        handleMouseout(link, previewContainer, removeMousemoveListener);
                        handleClick(link, previewContainer, removeMousemoveListener);
                    });
                } else {
                    console.log("Event listener already added for link:", link.href);
                }
            }
        });

        if (linksFound === 0) {
            console.warn("No suitable links found.");
        }
    }

    function shouldAddPreview(link) {
        const href = link.href;

        // Check if the link is in the exclusion list
        for (const exclusion of exclusionList) {
            if (href.endsWith(exclusion)) {
                return false;
            }
        }

        const pattern = /subdl.com/; // General pattern to match subdl.com links
        return pattern.test(href);
    }

    function createPreviewContainer() {
        console.log("Creating preview container.");
        let previewContainer = document.createElement("div");
        Object.assign(previewContainer.style, {
            position: "fixed",
            display: "none",
            transition: "opacity 0.1s ease-in-out",
            opacity: 0,
            width: "154px",
            height: "231px",
            overflow: "hidden",
            zIndex: 1000,
            borderRadius: "8px",
            boxShadow: "0 4px 8px rgba(0,0,0,0.2)",
            backgroundColor: "#ffffff"
        });
        console.log("Preview container created:", previewContainer);
        return previewContainer;
    }

    function showLoadingSpinner(previewContainer) {
        console.log("Showing loading spinner.");
        previewContainer.innerHTML = `
            <div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: #f0f0f0;">
                <div style="width: 40px; height: 40px; border: 4px solid #333; border-top: 4px solid #999; border-radius: 50%; animation: spin 1s linear infinite;"></div>
            </div>
            <style>
                @keyframes spin {
                    0% { transform: rotate(0deg); }
                    100% { transform: rotate(360deg); }
                }
            </style>
        `;
        previewContainer.style.display = "block";
        previewContainer.style.opacity = 1;
    }

    function fetchImage(url, previewContainer) {
        console.log("Fetching image for URL:", url);
        const cacheKey = storagePrefix + url;
        let cachedImage = localStorage.getItem(cacheKey);

        if (cachedImage) {
            try {
                const parsedCache = JSON.parse(cachedImage);
                if (Date.now() - parsedCache.timestamp < maxCacheAge) {
                    console.log(`Image found in cache for ${url}`);
                    setImage(previewContainer, parsedCache.src);
                    return;
                } else {
                    console.log(`Cached image for ${url} is too old, fetching new one`);
                    localStorage.removeItem(cacheKey);
                }
            } catch (e) {
                console.error(`Error parsing cached image for ${url}`, e);
                localStorage.removeItem(cacheKey);
            }
        }

        console.log(`Image not in cache, fetching from network for ${url}`);
        fetch(url)
            .then(response => response.text())
            .then(html => {
                console.log("Fetched HTML for URL:", url);
                let parser = new DOMParser();
                let doc = parser.parseFromString(html, 'text/html');
                let preview = doc.querySelector("div.select-none img"); // New image selector
                if (preview) {
                    let src = preview.getAttribute("src");
                    console.log(`Image fetched successfully for ${url}`);
                    setImage(previewContainer, src);
                    try {
                        localStorage.setItem(cacheKey, JSON.stringify({
                            src: src,
                            timestamp: Date.now()
                        }));
                    } catch (e) {
                        console.error(`Failed to cache image for ${url}:`, e);
                        clearOldCache(); // Attempt to free up space
                    }
                } else {
                    console.log(`Image not found in the fetched HTML for ${url}`);
                    setError(previewContainer, "Image not found.");
                }
            })
            .catch(error => {
                console.error(`Failed to fetch image for ${url}: ${error}`);
                setError(previewContainer, "Failed to load image.");
            });
    }

    function setImage(previewContainer, src) {
        console.log("Setting image for preview container:", src);
        previewContainer.innerHTML = `<img style="width: 100%; height: 100%; object-fit: cover;" src="${src}"/>`;
        previewContainer.style.display = "block";
        previewContainer.style.opacity = 1;
        console.log("Image displayed in preview container.");
    }

    function setError(previewContainer, message) {
        console.log("Setting error message for preview container:", message);
        previewContainer.innerHTML = `
            <div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; color: red; font-weight: bold; text-align: center;">
                ${message}
            </div>
        `;
        previewContainer.style.display = "block";
        previewContainer.style.opacity = 1;
        console.log("Error message displayed in preview container.");
    }

    function addMousemoveListener(previewContainer) {
        console.log("Adding mousemove listener for preview container.");
        function movePreview(event) {
            previewContainer.style.top = event.clientY + 20 + "px";
            previewContainer.style.left = event.clientX + 20 + "px";

            if (event.clientX + previewContainer.offsetWidth + 20 > window.innerWidth) {
                previewContainer.style.left = window.innerWidth - previewContainer.offsetWidth - 20 + "px";
            }
            if (event.clientY + previewContainer.offsetHeight + 20 > window.innerHeight) {
                previewContainer.style.top = window.innerHeight - previewContainer.offsetHeight - 20 + "px";
            }
        }

        document.addEventListener("mousemove", movePreview);
        console.log("Mousemove listener added.");

        return () => {
            document.removeEventListener("mousemove", movePreview);
            console.log("Mousemove listener removed.");
        };
    }

    function handleMouseout(link, previewContainer, removeMousemoveListener) {
        console.log("Adding mouseout listener for link:", link.href);
        link.addEventListener("mouseout", function() {
            console.log("Mouseout event triggered for link:", link.href);
            cleanupPreview(previewContainer, removeMousemoveListener);
        });
    }

    function handleClick(link, previewContainer, removeMousemoveListener) {
        console.log("Adding click listener for link:", link.href);
        link.addEventListener("click", function() {
            console.log("Click event triggered for link:", link.href);
            cleanupPreview(previewContainer, removeMousemoveListener);
        });
    }

    function cleanupPreview(previewContainer, removeMousemoveListener) {
        previewContainer.style.opacity = 0;
        setTimeout(() => {
            previewContainer.style.display = "none";
            previewContainer.remove();
            removeMousemoveListener();
            console.log("Preview container removed.");
        }, 200);
    }

    // Clear old cache entries and set up the DOM mutation observer
    clearOldCache();
    addPreviewToLinks();

    const observer = new MutationObserver(() => {
        console.log("DOM mutation detected, adding preview to new links.");
        addPreviewToLinks();
    });
    observer.observe(document.body, { childList: true, subtree: true });

})();