SubDL Enhanced - Dark Theme + Image Previews

Combined script: Forces dark theme, displays image previews on hover and beside titles with configuration options

// ==UserScript==
// @name         SubDL Enhanced - Dark Theme + Image Previews
// @namespace    http://tampermonkey.net/
// @version      2.0
// @description  Combined script: Forces dark theme, displays image previews on hover and beside titles with configuration options
// @author       dr.bobo0
// @license      MIT
// @match        https://subdl.com/*
// @match        https://*.subdl.com/*
// @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        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// ==/UserScript==

(function() {
    'use strict';

    // ===== DARK THEME SECTION =====
    // Force dark theme immediately
    function initDarkTheme() {
        try {
            localStorage.setItem('theme', 'dark');
        } catch (e) {
            console.error("SubDL Enhanced: Failed to set localStorage", e);
        }

        const applyDarkStyles = () => {
            if (document.documentElement) {
                document.documentElement.classList.add('dark');
                document.documentElement.classList.remove('light');
                document.documentElement.style.colorScheme = 'dark';
            }
        };

        applyDarkStyles();

        if (!document.documentElement || !document.documentElement.classList.contains('dark')) {
            const observer = new MutationObserver((mutationsList, obs) => {
                if (document.documentElement) {
                    applyDarkStyles();
                    obs.disconnect();
                }
            });
            observer.observe(document, { childList: true, subtree: true });
        }
    }

    // Initialize dark theme immediately
    initDarkTheme();

    // ===== IMAGE PREVIEW CONFIGURATION =====
    const DEFAULT_CONFIG = {
        imageWidth: 75,
        imageHeight: 112,
        isSquare: false,
        hideDownloadButton: false,
        enableHoverPreview: true,
        enableBesidePreview: true
    };

    function getSettings() {
        return {
            imageWidth: GM_getValue('imageWidth', DEFAULT_CONFIG.imageWidth),
            imageHeight: GM_getValue('imageHeight', DEFAULT_CONFIG.imageHeight),
            isSquare: GM_getValue('isSquare', DEFAULT_CONFIG.isSquare),
            hideDownloadButton: GM_getValue('hideDownloadButton', DEFAULT_CONFIG.hideDownloadButton),
            enableHoverPreview: GM_getValue('enableHoverPreview', DEFAULT_CONFIG.enableHoverPreview),
            enableBesidePreview: GM_getValue('enableBesidePreview', DEFAULT_CONFIG.enableBesidePreview)
        };
    }

    function saveSettings(settings) {
        Object.keys(settings).forEach(key => {
            GM_setValue(key, settings[key]);
        });
    }

    // ===== SHARED UTILITIES =====
    const storagePrefix = "subdl_image_cache_";
    const maxCacheAge = 7 * 24 * 60 * 60 * 1000; // 7 days

    const exclusionList = [
        '/', '/panel', '/panel/my-subtitles', '/panel/account', '/panel/api',
        '/latest', '/popular', 'https://t.me/subdl_com', '/ads', '/api-doc',
        '/panel/logout', '/login', '#', '/signup'
    ];

    function safeJSONParse(key) {
        try {
            const item = localStorage.getItem(key);
            return item ? JSON.parse(item) : null;
        } catch (e) {
            localStorage.removeItem(key);
            return null;
        }
    }

    function clearOldCache() {
        const now = Date.now();
        Object.keys(localStorage)
            .filter(key => key.startsWith(storagePrefix))
            .forEach(key => {
                const item = safeJSONParse(key);
                if (!item || now - item.timestamp > maxCacheAge) {
                    localStorage.removeItem(key);
                }
            });
    }

    function getImageConfig() {
        const settings = getSettings();
        return {
            IMAGE_STYLES: {
                width: `${settings.imageWidth}px`,
                height: `${settings.imageHeight}px`,
                objectFit: settings.isSquare ? 'cover' : 'contain',
                borderRadius: '4px',
                boxShadow: '0 2px 6px rgba(0, 0, 0, 0.15)',
                marginRight: '8px'
            },
            LINK_STYLES: {
                display: 'flex',
                alignItems: 'center',
                gap: '8px'
            }
        };
    }

    function applyStyles(element, styles) {
        Object.entries(styles).forEach(([key, value]) => {
            element.style[key] = value;
        });
    }

    function createElement(tag, styles = {}, attributes = {}) {
        const element = document.createElement(tag);
        applyStyles(element, styles);
        Object.entries(attributes).forEach(([key, value]) => {
            element[key] = value;
        });
        return element;
    }

    // ===== HOVER PREVIEW SECTION =====
    function shouldAddPreview(link) {
        const href = link.href;
        return !exclusionList.some(exclusion => href.endsWith(exclusion)) && /subdl.com/.test(href);
    }

    function createPreviewContainer() {
        const 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: "#21293b"
        });
        return previewContainer;
    }

    function showLoadingSpinner(previewContainer) {
        previewContainer.innerHTML = `
            <div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; background-color: #2d3748;">
                <div style="width: 40px; height: 40px; border: 4px solid #4a5568; border-top: 4px solid #718096; 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 fetchImageForHover(url, previewContainer) {
        const cacheKey = storagePrefix + url;
        const cachedImage = safeJSONParse(cacheKey);

        if (cachedImage && Date.now() - cachedImage.timestamp < maxCacheAge) {
            setImage(previewContainer, cachedImage.src);
            return;
        }

        fetch(url)
            .then(response => response.text())
            .then(html => {
                const doc = new DOMParser().parseFromString(html, 'text/html');
                const preview = doc.querySelector("div.select-none img");
                if (preview) {
                    const src = preview.getAttribute("src");
                    setImage(previewContainer, src);
                    try {
                        localStorage.setItem(cacheKey, JSON.stringify({ src, timestamp: Date.now() }));
                    } catch (e) {
                        clearOldCache();
                    }
                } else {
                    setError(previewContainer, "Image not found.");
                }
            })
            .catch(() => setError(previewContainer, "Failed to load image."));
    }

    function setImage(previewContainer, src) {
        previewContainer.innerHTML = `<img style="width: 100%; height: 100%; object-fit: cover;" src="${src}"/>`;
        previewContainer.style.display = "block";
        previewContainer.style.opacity = 1;
    }

    function setError(previewContainer, message) {
        previewContainer.innerHTML = `
            <div style="display: flex; justify-content: center; align-items: center; width: 100%; height: 100%; color: #fc8181; font-weight: bold; text-align: center; background-color: #2d3748;">
                ${message}
            </div>
        `;
        previewContainer.style.display = "block";
        previewContainer.style.opacity = 1;
    }

    function addMousemoveListener(previewContainer) {
        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);
        return () => document.removeEventListener("mousemove", movePreview);
    }

    function cleanupPreview(previewContainer, removeMousemoveListener) {
        previewContainer.style.opacity = 0;
        setTimeout(() => {
            if (previewContainer.parentNode) {
                previewContainer.remove();
            }
            removeMousemoveListener();
        }, 200);
    }

    function addHoverPreviewToLinks() {
        if (!getSettings().enableHoverPreview) return;

        const links = document.querySelectorAll('a[href*="/s/info/"]:not([data-hover-preview])');

        links.forEach(link => {
            if (shouldAddPreview(link)) {
                link.setAttribute('data-hover-preview', 'true');
                link.addEventListener("mouseover", function () {
                    const previewContainer = createPreviewContainer();
                    document.body.appendChild(previewContainer);
                    showLoadingSpinner(previewContainer);
                    fetchImageForHover(this.href, previewContainer);

                    const removeMousemoveListener = addMousemoveListener(previewContainer);

                    const handleMouseout = () => cleanupPreview(previewContainer, removeMousemoveListener);
                    const handleClick = () => cleanupPreview(previewContainer, removeMousemoveListener);

                    link.addEventListener("mouseout", handleMouseout, { once: true });
                    link.addEventListener("click", handleClick, { once: true });
                });
            }
        });
    }

    // ===== BESIDE PREVIEW SECTION =====
    async function fetchImageForBeside(url, container) {
        const fullUrl = new URL(url, window.location.origin).href;
        const cacheKey = storagePrefix + fullUrl;

        const cachedImage = safeJSONParse(cacheKey);
        if (cachedImage && Date.now() - cachedImage.timestamp < maxCacheAge) {
            displayImageBeside(cachedImage.src, container);
            return;
        }

        try {
            const response = await fetch(fullUrl);
            const html = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');
            const preview = doc.querySelector("div.select-none img");

            if (preview) {
                const src = preview.getAttribute("src");
                if (src) {
                    displayImageBeside(src, container);
                    localStorage.setItem(cacheKey, JSON.stringify({
                        src: src,
                        timestamp: Date.now()
                    }));
                }
            }
        } catch (error) {
            console.error(`Failed to fetch image for ${fullUrl}:`, error);
        }
    }

    function displayImageBeside(src, container) {
        const CONFIG = getImageConfig();
        const img = document.createElement("img");
        img.src = src;
        img.alt = "Preview";
        img.setAttribute('data-subdl-preview', 'true');

        Object.entries(CONFIG.IMAGE_STYLES).forEach(([key, value]) => {
            img.style[key] = value;
        });

        img.onerror = () => {
            img.src = 'https://subdl.com/images/poster.jpeg';
        };

        container.parentElement.insertBefore(img, container);
    }

    function addBesideImagePreviews() {
        if (!getSettings().enableBesidePreview) return;

        const links = document.querySelectorAll('a[href^="/s/info/"]:not([data-beside-preview])');
        const CONFIG = getImageConfig();

        links.forEach(link => {
            link.setAttribute('data-beside-preview', 'true');

            const container = link.querySelector('h3');
            if (!container) return;

            applyStyles(link, CONFIG.LINK_STYLES);
            const parentDiv = link.closest('.flex');
            if (parentDiv) {
                applyStyles(parentDiv, {
                    display: 'flex',
                    alignItems: 'center',
                    gap: '12px'
                });
            }

            const svgIcon = link.querySelector('svg');
            if (svgIcon) {
                applyStyles(svgIcon, {
                    width: '20px',
                    height: '20px',
                    marginRight: '8px',
                    verticalAlign: 'middle'
                });
            }

            fetchImageForBeside(link.href, container);
        });
    }

    // ===== SETTINGS MODAL =====
    const COLORS = {
        background: '#1a202c',
        modalBg: '#2d3748',
        primary: '#4299e1',
        secondary: '#718096',
        accent: '#48bb78',
        textPrimary: '#e2e8f0',
        textSecondary: '#a0aec0',
        borderColor: '#4a5568'
    };

    function createSettingsModal() {
        const backdrop = createElement('div', {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            background: 'rgba(0,0,0,0.5)',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            zIndex: '10000',
            fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif'
        });

        const modal = createElement('div', {
            background: COLORS.modalBg,
            padding: '30px',
            borderRadius: '16px',
            width: '500px',
            boxShadow: '0 25px 50px -12px rgba(0,0,0,0.15)',
            position: 'relative',
            maxHeight: '80vh',
            overflowY: 'auto'
        });

        const title = createElement('h2', {
            color: COLORS.primary,
            marginBottom: '25px',
            textAlign: 'center',
            fontWeight: '700',
            fontSize: '1.5rem'
        }, { textContent: 'SubDL Enhancement Settings' });
        modal.appendChild(title);

        const settings = getSettings();

        // Preview Container
        const previewContainer = createElement('div', {
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            marginBottom: '25px',
            background: COLORS.background,
            padding: '25px',
            borderRadius: '12px'
        });
        modal.appendChild(previewContainer);

        const previewImg = createElement('img', {
            transition: 'all 0.3s ease',
            borderRadius: '12px',
            boxShadow: '0 10px 25px rgba(0,0,0,0.1)'
        }, { src: 'https://via.placeholder.com/150x225?text=Preview' });
        previewContainer.appendChild(previewImg);

        const settingsContainer = createElement('div', {
            display: 'flex',
            flexDirection: 'column',
            gap: '20px'
        });
        modal.appendChild(settingsContainer);

        // Preview Mode Toggles
        const previewModesContainer = createElement('div', {
            display: 'flex',
            flexDirection: 'column',
            gap: '12px'
        });

        const hoverToggleContainer = createElement('div', {
            display: 'flex',
            alignItems: 'center',
            gap: '12px'
        });

        const hoverToggle = createElement('input', {
            accentColor: COLORS.primary
        }, {
            type: 'checkbox',
            id: 'hoverPreviewToggle',
            checked: settings.enableHoverPreview
        });

        const hoverToggleLabel = createElement('label', {
            color: COLORS.textSecondary
        }, {
            htmlFor: 'hoverPreviewToggle',
            textContent: 'Enable Hover Previews'
        });

        hoverToggleContainer.appendChild(hoverToggle);
        hoverToggleContainer.appendChild(hoverToggleLabel);

        const besideToggleContainer = createElement('div', {
            display: 'flex',
            alignItems: 'center',
            gap: '12px'
        });

        const besideToggle = createElement('input', {
            accentColor: COLORS.primary
        }, {
            type: 'checkbox',
            id: 'besidePreviewToggle',
            checked: settings.enableBesidePreview
        });

        const besideToggleLabel = createElement('label', {
            color: COLORS.textSecondary
        }, {
            htmlFor: 'besidePreviewToggle',
            textContent: 'Enable Beside Title Previews'
        });

        besideToggleContainer.appendChild(besideToggle);
        besideToggleContainer.appendChild(besideToggleLabel);

        previewModesContainer.appendChild(hoverToggleContainer);
        previewModesContainer.appendChild(besideToggleContainer);
        settingsContainer.appendChild(previewModesContainer);

        // Image Width Slider
        const widthContainer = createElement('div');
        const widthLabel = createElement('label', {
            display: 'flex',
            justifyContent: 'space-between',
            color: COLORS.textPrimary,
            fontWeight: '600'
        });

        const widthLabelText = createElement('span', {}, { textContent: 'Image Width' });
        const widthValue = createElement('span', {}, { textContent: `${settings.imageWidth}px` });
        widthLabel.appendChild(widthLabelText);
        widthLabel.appendChild(widthValue);

        const widthSlider = createElement('input', {
            width: '100%',
            accentColor: COLORS.primary
        }, {
            type: 'range',
            min: '50',
            max: '200',
            value: settings.imageWidth
        });

        function updatePreview() {
            const width = widthSlider.value;
            const height = width * (squareToggle.checked ? 1 : 1.5);

            widthValue.textContent = `${width}px`;
            previewImg.style.width = `${width}px`;
            previewImg.style.height = `${height}px`;

            document.querySelectorAll('img[data-subdl-preview]').forEach(img => {
                img.style.width = `${width}px`;
                img.style.height = `${height}px`;
            });
        }

        widthSlider.oninput = updatePreview;

        widthContainer.appendChild(widthLabel);
        widthContainer.appendChild(widthSlider);
        settingsContainer.appendChild(widthContainer);

        // Square Images Toggle
        const squareToggleContainer = createElement('div', {
            display: 'flex',
            alignItems: 'center',
            gap: '12px'
        });

        const squareToggle = createElement('input', {
            accentColor: COLORS.primary
        }, {
            type: 'checkbox',
            id: 'squareImagesToggle',
            checked: settings.isSquare
        });

        const squareToggleLabel = createElement('label', {
            color: COLORS.textSecondary
        }, {
            htmlFor: 'squareImagesToggle',
            textContent: 'Square Images'
        });

        squareToggle.oninput = () => {
            const width = widthSlider.value;
            const height = width * (squareToggle.checked ? 1 : 1.5);

            previewImg.style.height = `${height}px`;

            document.querySelectorAll('img[data-subdl-preview]').forEach(img => {
                img.style.height = `${height}px`;
                img.style.objectFit = squareToggle.checked ? 'cover' : 'contain';
            });
        };

        squareToggleContainer.appendChild(squareToggle);
        squareToggleContainer.appendChild(squareToggleLabel);
        settingsContainer.appendChild(squareToggleContainer);

        // Download Button Toggle
        const downloadToggleContainer = createElement('div', {
            display: 'flex',
            alignItems: 'center',
            gap: '12px'
        });

        const downloadToggle = createElement('input', {
            accentColor: COLORS.primary
        }, {
            type: 'checkbox',
            id: 'hideDownloadToggle',
            checked: settings.hideDownloadButton
        });

        const downloadToggleLabel = createElement('label', {
            color: COLORS.textSecondary
        }, {
            htmlFor: 'hideDownloadToggle',
            textContent: 'Hide Download Buttons'
        });

        downloadToggle.oninput = () => {
            const downloadButtons = document.querySelectorAll('a[href^="https://dl.subdl.com/subtitle/"]');
            downloadButtons.forEach(button => {
                button.style.display = downloadToggle.checked ? 'none' : '';
            });
        };

        downloadToggleContainer.appendChild(downloadToggle);
        downloadToggleContainer.appendChild(downloadToggleLabel);
        settingsContainer.appendChild(downloadToggleContainer);

        // Buttons
        const buttonContainer = createElement('div', {
            display: 'flex',
            justifyContent: 'space-between',
            marginTop: '25px',
            gap: '15px'
        });

        const saveButton = createElement('button', {
            backgroundColor: COLORS.accent,
            color: 'white',
            border: 'none',
            padding: '12px 20px',
            borderRadius: '8px',
            cursor: 'pointer',
            flexGrow: '1',
            fontWeight: '600'
        }, { textContent: 'Save Settings' });

        saveButton.onclick = () => {
            const newSettings = {
                imageWidth: parseInt(widthSlider.value),
                imageHeight: parseInt(widthSlider.value) * (squareToggle.checked ? 1 : 1.5),
                isSquare: squareToggle.checked,
                hideDownloadButton: downloadToggle.checked,
                enableHoverPreview: hoverToggle.checked,
                enableBesidePreview: besideToggle.checked
            };
            saveSettings(newSettings);
            document.body.removeChild(backdrop);

            // Apply settings immediately
            applyDownloadButtonVisibility();
            refreshPreviews();
        };

        const cancelButton = createElement('button', {
            backgroundColor: COLORS.background,
            color: COLORS.textSecondary,
            border: `2px solid ${COLORS.background}`,
            padding: '10px 20px',
            borderRadius: '8px',
            cursor: 'pointer',
            flexGrow: '1',
            fontWeight: '600'
        }, { textContent: 'Cancel' });

        cancelButton.onclick = () => {
            document.body.removeChild(backdrop);
        };

        buttonContainer.appendChild(saveButton);
        buttonContainer.appendChild(cancelButton);
        settingsContainer.appendChild(buttonContainer);

        backdrop.appendChild(modal);
        document.body.appendChild(backdrop);

        updatePreview();
    }

    // ===== UTILITY FUNCTIONS =====
    function applyDownloadButtonVisibility() {
        const settings = getSettings();
        const downloadButtons = document.querySelectorAll('a[href^="https://dl.subdl.com/subtitle/"]');
        downloadButtons.forEach(button => {
            button.style.display = settings.hideDownloadButton ? 'none' : '';
        });
    }

    function refreshPreviews() {
        // Remove existing previews
        document.querySelectorAll('img[data-subdl-preview]').forEach(img => img.remove());
        document.querySelectorAll('a[data-beside-preview]').forEach(link => link.removeAttribute('data-beside-preview'));
        document.querySelectorAll('a[data-hover-preview]').forEach(link => link.removeAttribute('data-hover-preview'));

        // Re-add previews
        addHoverPreviewToLinks();
        addBesideImagePreviews();
    }

    function throttle(func, limit) {
        let lastFunc;
        let lastRan;
        return function() {
            const context = this;
            const args = arguments;
            if (!lastRan) {
                func.apply(context, args);
                lastRan = Date.now();
            } else {
                clearTimeout(lastFunc);
                lastFunc = setTimeout(function() {
                    if (Date.now() - lastRan >= limit) {
                        func.apply(context, args);
                        lastRan = Date.now();
                    }
                }, limit - (Date.now() - lastRan));
            }
        };
    }

    // ===== INITIALIZATION =====
    function init() {
        // Wait for DOM to be ready
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', initFeatures);
        } else {
            initFeatures();
        }
    }

    function initFeatures() {
        clearOldCache();
        addHoverPreviewToLinks();
        addBesideImagePreviews();
        applyDownloadButtonVisibility();

        const throttledUpdate = throttle(() => {
            addHoverPreviewToLinks();
            addBesideImagePreviews();
            applyDownloadButtonVisibility();
        }, 500);

        const observer = new MutationObserver(() => {
            throttledUpdate();
        });

        observer.observe(document.body, {
            childList: true,
            subtree: true
        });
    }

    // Register menu command
    GM_registerMenuCommand('Configure SubDL Enhancements', createSettingsModal);

    // Start the script
    init();
})();