SubsPlease Enhanced Image Previews

Show image previews next to the anime titles with advanced Tampermonkey settings

// ==UserScript==
// @name         SubsPlease Enhanced Image Previews
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Show image previews next to the anime titles with advanced Tampermonkey settings
// @author       dr.bobo0
// @license      MIT
// @match        https://subsplease.org/
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_addStyle
// @grant        GM_getResourceText
// @grant        GM_addElement
// ==/UserScript==

(function() {
    'use strict';

    // Configuration object with Tampermonkey-backed settings
    const CONFIG = {
        imageWidth: GM_getValue('imageWidth', 100),
        makeSquare: GM_getValue('makeSquare', false),
        enableHoverEffects: GM_getValue('enableHoverEffects', true),

        saveSettings(key, value) {
            GM_setValue(key, value);
            this[key] = value;
        }
    };

    // Optimized update function with better transitions
    function updateImagesOnPage() {
        const images = document.querySelectorAll('.has-image img');
        if (!images.length) return;

        requestAnimationFrame(() => {
            images.forEach(img => {
                // Reset any previously set dimensions
                img.removeAttribute('style');

                // Apply new styles with smoother transitions
                img.style.cssText = `
                    width: ${CONFIG.imageWidth}px;
                    height: ${CONFIG.makeSquare ? `${CONFIG.imageWidth}px` : 'auto'};
                    cursor: pointer;
                    transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
                    border-radius: 4px;
                    object-fit: ${CONFIG.makeSquare ? 'cover' : 'contain'};
                    max-width: none;
                    will-change: transform, width, height;
                    backface-visibility: hidden;
                    transform: translateZ(0);
                `;

                if (CONFIG.enableHoverEffects) {
                    const applyHoverEffect = () => {
                        img.style.transform = 'scale(1.1) translateZ(0)';
                        img.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
                    };

                    const removeHoverEffect = () => {
                        img.style.transform = 'scale(1) translateZ(0)';
                        img.style.boxShadow = 'none';
                    };

                    img.addEventListener('mouseenter', applyHoverEffect);
                    img.addEventListener('mouseleave', removeHoverEffect);

                    // Store event listeners for cleanup
                    img._hoverListeners = {
                        enter: applyHoverEffect,
                        leave: removeHoverEffect
                    };
                } else if (img._hoverListeners) {
                    // Clean up old listeners
                    img.removeEventListener('mouseenter', img._hoverListeners.enter);
                    img.removeEventListener('mouseleave', img._hoverListeners.leave);
                    delete img._hoverListeners;
                }

                // Update the containing cell width with transition
                const cell = img.parentElement;
                if (cell && cell.tagName === 'TD') {
                    cell.style.cssText = `
                        padding-right: 10px;
                        vertical-align: middle;
                        width: ${CONFIG.imageWidth + 10}px;
                        transition: width 0.15s cubic-bezier(0.4, 0, 0.2, 1);
                    `;
                }
            });
        });
    }

    function createSettingsUI() {
        // Store original values when opening settings
        const originalValues = {
            imageWidth: CONFIG.imageWidth,
            makeSquare: CONFIG.makeSquare,
            enableHoverEffects: CONFIG.enableHoverEffects
        };

        GM_addStyle(`
            #subsplease-settings-container {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: #1e1e1e;
                color: #fff;
                border: 2px solid #333;
                border-radius: 10px;
                padding: 20px;
                width: 400px;
                max-width: 90%;
                box-shadow: 0 4px 6px rgba(0,0,0,0.4);
                z-index: 10000;
            }
            #subsplease-settings-container h2 {
                margin-top: 0;
                border-bottom: 1px solid #444;
                padding-bottom: 10px;
            }
            .setting-row {
                display: flex;
                align-items: center;
                margin-bottom: 15px;
                gap: 10px;
            }
            .setting-row label {
                flex-grow: 1;
            }
            #preview-image {
                max-width: 100%;
                border-radius: 4px;
                margin-top: 10px;
            }
            .button-row {
                display: flex;
                justify-content: space-between;
                margin-top: 15px;
            }
            .button-row button {
                background: #444;
                color: #fff;
                border: none;
                border-radius: 4px;
                padding: 8px 12px;
                cursor: pointer;
                transition: background 0.3s;
            }
            .button-row button:hover {
                background: #555;
            }
            #image-width-slider {
                width: 200px;
                cursor: pointer;
            }
        `);

        const settingsContainer = document.createElement('div');
        settingsContainer.id = 'subsplease-settings-container';
        settingsContainer.innerHTML = `
            <h2>SubsPlease Image Preview Settings</h2>

            <div class="setting-row">
                <label for="image-width-slider">Image Width: <span id="width-value">${CONFIG.imageWidth}px</span></label>
                <input type="range" id="image-width-slider" min="50" max="300" step="10" value="${CONFIG.imageWidth}">
            </div>

            <div class="setting-row">
                <label for="square-images-toggle">Square Images</label>
                <input type="checkbox" id="square-images-toggle" ${CONFIG.makeSquare ? 'checked' : ''}>
            </div>

            <div class="setting-row">
                <label for="hover-effects-toggle">Hover Effects</label>
                <input type="checkbox" id="hover-effects-toggle" ${CONFIG.enableHoverEffects ? 'checked' : ''}>
            </div>

            <div class="button-row">
                <button id="save-settings">Save</button>
                <button id="close-settings">Cancel</button>
            </div>
        `;

        document.body.appendChild(settingsContainer);

        const widthSlider = document.getElementById('image-width-slider');
        const widthValue = document.getElementById('width-value');
        const squareToggle = document.getElementById('square-images-toggle');
        const hoverToggle = document.getElementById('hover-effects-toggle');
        const saveButton = document.getElementById('save-settings');
        const closeButton = document.getElementById('close-settings');

        // Optimized slider update with debounce
        const smoothUpdate = debounce((value) => {
            CONFIG.imageWidth = parseInt(value);
            updateImagesOnPage();
        }, 10);

        // Show live changes on slider and toggles
        widthSlider.addEventListener('input', (e) => {
            widthValue.textContent = `${e.target.value}px`;
            smoothUpdate(e.target.value);
        });

        squareToggle.addEventListener('change', () => {
            CONFIG.makeSquare = squareToggle.checked;
            updateImagesOnPage();
        });

        hoverToggle.addEventListener('change', () => {
            CONFIG.enableHoverEffects = hoverToggle.checked;
            updateImagesOnPage();
        });

        saveButton.addEventListener('click', () => {
            CONFIG.saveSettings('imageWidth', parseInt(widthSlider.value));
            CONFIG.saveSettings('makeSquare', squareToggle.checked);
            CONFIG.saveSettings('enableHoverEffects', hoverToggle.checked);
            settingsContainer.remove();
        });

        closeButton.addEventListener('click', () => {
            // Restore original values when canceling
            CONFIG.imageWidth = originalValues.imageWidth;
            CONFIG.makeSquare = originalValues.makeSquare;
            CONFIG.enableHoverEffects = originalValues.enableHoverEffects;

            // Update the page with restored values
            updateImagesOnPage();

            // Remove the settings container
            settingsContainer.remove();
        });
    }

    GM_registerMenuCommand('Configure Image Previews', createSettingsUI);

    const injectImages = debounce(() => {
        const rows = document.querySelectorAll(".frontpage-releases-container tr:not(.has-image)");

        rows.forEach(row => {
            try {
                const name = row.querySelector(".release-item a");
                const { previewImage } = name.dataset;

                if (!previewImage) return;

                const img = document.createElement("img");
                img.src = previewImage;
                img.alt = name.textContent + " preview";

                img.style.cssText = `
                    width: ${CONFIG.imageWidth}px;
                    height: ${CONFIG.makeSquare ? `${CONFIG.imageWidth}px` : 'auto'};
                    cursor: pointer;
                    transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
                    border-radius: 4px;
                    object-fit: ${CONFIG.makeSquare ? 'cover' : 'contain'};
                    max-width: none;
                    will-change: transform, width, height;
                    backface-visibility: hidden;
                    transform: translateZ(0);
                `;

                if (CONFIG.enableHoverEffects) {
                    img.addEventListener('mouseenter', () => {
                        img.style.transform = 'scale(1.1) translateZ(0)';
                        img.style.boxShadow = '0 4px 6px rgba(0,0,0,0.1)';
                    });
                    img.addEventListener('mouseleave', () => {
                        img.style.transform = 'scale(1) translateZ(0)';
                        img.style.boxShadow = 'none';
                    });
                }

                img.addEventListener('click', (e) => {
                    e.preventDefault();
                    window.location.href = name.href;
                });

                const td = document.createElement("td");
                td.style.cssText = `
                    padding-right: 10px;
                    vertical-align: middle;
                    width: ${CONFIG.imageWidth + 10}px;
                    transition: width 0.15s cubic-bezier(0.4, 0, 0.2, 1);
                `;
                td.appendChild(img);

                row.insertBefore(td, row.querySelector("td:first-child"));
                row.classList.add('has-image');

                const info = row.querySelector(".release-item-time");
                if (info) {
                    info.style.verticalAlign = "top";
                }
            } catch (error) {
                console.warn('Error processing row:', error);
            }
        });
    }, 300);

    injectImages();

    const loadMoreButton = document.querySelector("#latest-load-more span");
    if (loadMoreButton) {
        loadMoreButton.addEventListener('click', injectImages);
    }

    const observer = new MutationObserver((mutations) => {
        for (const mutation of mutations) {
            if (mutation.type === 'childList') {
                injectImages();
                break;
            }
        }
    });

    const container = document.querySelector(".frontpage-releases-container");
    if (container) {
        observer.observe(container, {
            childList: true,
            subtree: true
        });
    }

    function debounce(func, wait) {
        let timeout;
        return function(...args) {
            const later = () => {
                clearTimeout(timeout);
                func(...args);
            };
            clearTimeout(timeout);
            timeout = setTimeout(later, wait);
        };
    }
})();