JAVLibrary Full Covers + Grid Control

Improves thumbnail image quality and adjusts cards per row for javlibrary.com. Features: clearer full-size covers, custom grid (3-16 cards/row), saves settings, full-screen gallery view, quick-search buttons.

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name         JAVLibrary Full Covers + Grid Control
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Improves thumbnail image quality and adjusts cards per row for javlibrary.com. Features: clearer full-size covers, custom grid (3-16 cards/row), saves settings, full-screen gallery view, quick-search buttons.
// @match        https://www.javlibrary.com/*
// @match        https://javlibrary.com/*
// @run-at       document-end
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    "use strict";

    // --- Helper: Debounce ---
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    // --- Change thumb to full cover ---
    function upgradeImages() {
        // javlibrary thumbs use "ps.jpg" suffix — upgrade to "pl.jpg" (full cover)
        document.querySelectorAll("div.video img").forEach((img) => {
            const src = img.src || img.getAttribute("src") || "";
            if (src.includes("ps.jpg")) {
                img.src = src.replace("ps.jpg", "pl.jpg");
            }
        });
    }

    // --- Grid Control ---
    const KEY = "javlib_cards_per_row";
    const loadCfg = () => parseInt(localStorage.getItem(KEY) || "6", 10);
    const saveCfg = (n) => localStorage.setItem(KEY, n.toString());

    let cardsPerRow = loadCfg();

    function applyGrid() {
        const cardWidth = 100 / cardsPerRow;
        const styleId = "javlib-grid-style";
        let style = document.getElementById(styleId);
        if (!style) {
            style = document.createElement("style");
            style.id = styleId;
            style.type = "text/css";
            document.head.appendChild(style);
        }

        style.textContent = `
    /* Grid container */
    div.videos {
        display: flex !important;
        flex-wrap: wrap !important;
        gap: 0 !important;
    }

    /* Each video card */
    div.video {
        flex: 0 0 ${cardWidth}% !important;
        max-width: ${cardWidth}% !important;
        box-sizing: border-box !important;
        padding: 4px !important;
        position: relative !important;
        margin: 0 !important;
        float: none !important;
    }

    div.video > a {
        display: block !important;
        text-decoration: none !important;
    }

    /* Image fit */
    div.video img {
        width: 100% !important;
        height: auto !important;
        object-fit: cover !important;
        border-radius: 6px 6px 0 0 !important;
        display: block !important;
    }

    /* Movie ID label */
    div.video .id {
        font-size: 0.8em !important;
        font-weight: 600 !important;
        padding: 4px 6px !important;
        color: #ff6b35 !important;
        background: rgba(0,0,0,0.6) !important;
        position: absolute !important;
        top: 4px !important;
        left: 4px !important;
        border-radius: 4px !important;
        z-index: 5 !important;
    }

    /* Title text */
    div.video .title.post_title {
        font-size: 0.75em !important;
        line-height: 1.3 !important;
        padding: 4px 6px !important;
        max-height: 3.9em !important;
        overflow: hidden !important;
        text-overflow: ellipsis !important;
        display: -webkit-box !important;
        -webkit-line-clamp: 3 !important;
        -webkit-box-orient: vertical !important;
        background: rgba(0,0,0,0.7) !important;
        color: #ddd !important;
        border-radius: 0 0 6px 6px !important;
    }

    /* Hide original toolbar */
    div.video .toolbar {
        display: none !important;
    }

    /* Tool Button Group */
    .javlib-tool-group {
        position: absolute;
        top: 8px;
        right: 8px;
        display: none;
        gap: 4px;
        z-index: 10;
        opacity: 0.6;
        transition: opacity 0.2s;
    }

    div.video:hover .javlib-tool-group {
        display: flex !important;
        opacity: 1;
    }

    .javlib-tool-btn {
        background: rgba(0, 0, 0, 0.7);
        color: white;
        border: 1px solid rgba(255, 255, 255, 0.3);
        border-radius: 4px;
        padding: 4px 6px;
        cursor: pointer;
        font-size: 12px;
        text-decoration: none;
        display: flex;
        align-items: center;
        justify-content: center;
    }

    .javlib-tool-btn:hover {
        background: #ff6b35;
        border-color: #ff6b35;
        color: white;
    }
`;
    }

    function createPanel() {
        const oldPanel = document.getElementById("javlib-grid-panel");
        if (oldPanel) oldPanel.remove();

        const panel = document.createElement("div");
        panel.id = "javlib-grid-panel";
        panel.innerHTML = `
            <div style="
                position: fixed;
                top: 10px;
                right: 10px;
                z-index: 99999;
                background: linear-gradient(135deg, #1a1a1a, #2d2d2d);
                color: #fff;
                padding: 12px;
                font-size: 13px;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
                border-radius: 8px;
                box-shadow: 0 4px 20px rgba(0,0,0,.6);
                min-width: 90px;
                backdrop-filter: blur(10px);
                border: 1px solid rgba(255,255,255,.1);
            ">
                <div style="margin-bottom: 8px; font-weight: 600; font-size: 14px;">
                    Cards/Row
                </div>
                <div style="display: flex; gap: 4px; justify-content: center; flex-wrap: wrap;">
                    ${[3, 4, 6, 8, 10, 12, 16]
                .map(
                    (n) => `
                        <button id="javlib-row-${n}"
                                style="
                                    padding: 6px 10px;
                                    font-size: 12px;
                                    font-weight: 500;
                                    border-radius: 6px;
                                    border: none;
                                    cursor: pointer;
                                    background: ${cardsPerRow === n ? "#ff6b35" : "rgba(255,255,255,.1)"};
                                    color: ${cardsPerRow === n ? "#fff" : "#ccc"};
                                    transition: all .2s ease;
                                    min-width: 32px;
                                "
                                title="Set ${n} cards per row"
                        >${n}</button>
                    `,
                )
                .join("")}
                </div>
                <div style="text-align: center; margin-top: 10px; font-size: 11px;">
                    <button id="javlib-grid-close"
                            style="background: none; border: none; color: #aaa; cursor: pointer; font-size: 16px; padding: 0 4px;">
                        ✕
                    </button>
                </div>
            </div>
        `;
        document.body.appendChild(panel);

        [3, 4, 6, 8, 10, 12, 16].forEach((n) => {
            document
                .getElementById(`javlib-row-${n}`)
                .addEventListener("click", () => {
                    cardsPerRow = n;
                    saveCfg(n);
                    applyGrid();
                    createPanel();
                });
        });

        document
            .getElementById("javlib-grid-close")
            .addEventListener("click", () => {
                panel.remove();
            });
    }

    // --- Gallery Feature ---
    function setupGalleryFeature() {
        // 1. Create Modal
        let modal = document.getElementById("javlib-gallery-modal");
        let content, prevBtn, nextBtn;

        if (!modal) {
            modal = document.createElement("div");
            modal.id = "javlib-gallery-modal";
            Object.assign(modal.style, {
                position: "fixed",
                top: "0",
                left: "0",
                width: "100%",
                height: "100%",
                zIndex: "1000000",
                background: "rgba(0, 0, 0, 0.95)",
                display: "none",
                flexDirection: "column",
                alignItems: "center",
                justifyContent: "center",
                backdropFilter: "blur(5px)",
            });

            // Inner container for images (Horizontal Scroll)
            content = document.createElement("div");
            content.id = "javlib-gallery-content";
            Object.assign(content.style, {
                display: "flex",
                flexDirection: "row",
                overflowX: "auto",
                overflowY: "hidden",
                scrollSnapType: "x mandatory",
                scrollBehavior: "smooth",
                width: "100%",
                height: "100%",
                alignItems: "center",
                justifyContent: "flex-start",
                padding: "0",
            });

            // Hide scrollbar but keep functionality
            const scrollStyle = document.createElement("style");
            scrollStyle.textContent = `
        #javlib-gallery-content::-webkit-scrollbar { display: none; }
        #javlib-gallery-content { -ms-overflow-style: none; scrollbar-width: none; }
      `;
            document.head.appendChild(scrollStyle);

            // Close button
            const closeBtn = document.createElement("button");
            closeBtn.innerHTML = "✕ Close (Esc)";
            Object.assign(closeBtn.style, {
                position: "fixed",
                top: "20px",
                right: "30px",
                background: "rgba(0, 0, 0, 0.5)",
                color: "#fff",
                border: "1px solid rgba(255, 255, 255, 0.3)",
                borderRadius: "20px",
                padding: "8px 16px",
                cursor: "pointer",
                zIndex: "1000002",
                fontSize: "14px",
            });
            closeBtn.addEventListener("click", closeModal);

            // Navigation Buttons
            const navBtnStyle = {
                position: "fixed",
                top: "50%",
                transform: "translateY(-50%)",
                background: "rgba(0, 0, 0, 0.3)",
                color: "white",
                border: "none",
                fontSize: "40px",
                padding: "20px",
                cursor: "pointer",
                zIndex: "1000002",
                transition: "background 0.2s",
                borderRadius: "50%",
                width: "80px",
                height: "80px",
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                userSelect: "none",
            };

            prevBtn = document.createElement("button");
            prevBtn.innerHTML = "‹";
            Object.assign(prevBtn.style, { ...navBtnStyle, left: "20px" });
            prevBtn.addEventListener(
                "mouseover",
                () => (prevBtn.style.background = "rgba(255, 107, 53, 0.8)"),
            );
            prevBtn.addEventListener(
                "mouseout",
                () => (prevBtn.style.background = "rgba(0, 0, 0, 0.3)"),
            );
            prevBtn.addEventListener("click", (e) => {
                e.stopPropagation();
                scrollGallery(-1);
            });

            nextBtn = document.createElement("button");
            nextBtn.innerHTML = "›";
            Object.assign(nextBtn.style, { ...navBtnStyle, right: "20px" });
            nextBtn.addEventListener(
                "mouseover",
                () => (nextBtn.style.background = "rgba(255, 107, 53, 0.8)"),
            );
            nextBtn.addEventListener(
                "mouseout",
                () => (nextBtn.style.background = "rgba(0, 0, 0, 0.3)"),
            );
            nextBtn.addEventListener("click", (e) => {
                e.stopPropagation();
                scrollGallery(1);
            });

            modal.appendChild(closeBtn);
            modal.appendChild(prevBtn);
            modal.appendChild(nextBtn);
            modal.appendChild(content);
            document.body.appendChild(modal);

            // --- Drag to Scroll State ---
            let isDown = false;
            let startX;
            let scrollLeft;
            let hasDragged = false;

            // Close on background click
            modal.addEventListener("click", (e) => {
                if (hasDragged) {
                    e.stopPropagation();
                    return;
                }
                if (e.target === modal || e.target === content) closeModal();
            });

            // --- Drag Listeners ---
            content.addEventListener("mousedown", (e) => {
                if (e.target.tagName === "IMG") {
                    e.preventDefault();
                }
                isDown = true;
                hasDragged = false;
                content.style.cursor = "grabbing";
                content.style.scrollSnapType = "none";
                content.style.scrollBehavior = "auto";
                startX = e.pageX - content.offsetLeft;
                scrollLeft = content.scrollLeft;
            });

            content.addEventListener("mouseleave", () => {
                if (!isDown) return;
                isDown = false;
                content.style.cursor = "grab";
                content.style.scrollSnapType = "x mandatory";
                content.style.scrollBehavior = "smooth";
            });

            content.addEventListener("mouseup", () => {
                if (!isDown) return;
                isDown = false;
                content.style.cursor = "grab";
                content.style.scrollSnapType = "x mandatory";
                content.style.scrollBehavior = "smooth";
            });

            content.addEventListener("mousemove", (e) => {
                if (!isDown) return;
                e.preventDefault();
                const x = e.pageX - content.offsetLeft;
                const walk = (x - startX) * 2;

                if (Math.abs(walk) > 5) {
                    hasDragged = true;
                }

                content.scrollLeft = scrollLeft - walk;
            });

            // --- Mouse Wheel to Horizontal Scroll ---
            let wheelTimeout;

            content.addEventListener(
                "wheel",
                (e) => {
                    e.preventDefault();

                    if (content.style.scrollSnapType !== "none") {
                        content.style.scrollSnapType = "none";
                        content.style.scrollBehavior = "auto";
                    }

                    const scrollSpeed = 2.5;
                    content.scrollLeft += e.deltaY * scrollSpeed;

                    clearTimeout(wheelTimeout);
                    wheelTimeout = setTimeout(() => {
                        content.style.scrollSnapType = "x mandatory";
                        content.style.scrollBehavior = "smooth";
                    }, 500);
                },
                { passive: false },
            );

            // Initial cursor
            content.style.cursor = "grab";
        } else {
            content = document.getElementById("javlib-gallery-content");
        }

        function closeModal() {
            if (modal) {
                modal.style.display = "none";
                content.innerHTML = "";
                document.body.style.overflow = "";
            }
        }

        function scrollGallery(direction) {
            const scrollAmount = window.innerWidth * 0.8;
            content.scrollBy({
                left: direction * scrollAmount,
                behavior: "smooth",
            });
        }

        // Handle Keys
        document.addEventListener("keydown", (e) => {
            if (modal.style.display !== "none") {
                if (e.key === "Escape") closeModal();
                if (e.key === "ArrowLeft") scrollGallery(-1);
                if (e.key === "ArrowRight") scrollGallery(1);
            }
        });

        const cache = window.javlib_gallery_cache || new Map();
        window.javlib_gallery_cache = cache;

        // 2. Add Icons to Cards
        function injectIcons() {
            document.querySelectorAll("div.video:not(.gallery-ready)").forEach((card) => {
                card.classList.add("gallery-ready");
                const link = card.querySelector("a.post-headline") || card.querySelector("a[href]");
                if (!link) return;

                // Extract movie code from the .id div
                let movieCode = "";
                const idDiv = card.querySelector(".id");
                if (idDiv) {
                    movieCode = idDiv.textContent.trim();
                }

                // Create Container
                const container = document.createElement("div");
                container.className = "javlib-tool-group";

                // --- Gallery Button ---
                const galleryBtn = document.createElement("div");
                galleryBtn.className = "javlib-tool-btn";
                galleryBtn.innerHTML = "📷";
                galleryBtn.title = "View Gallery";
                galleryBtn.addEventListener("click", async (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    // Show Modal with Loading
                    modal.style.display = "flex";
                    document.body.style.overflow = "hidden";
                    content.innerHTML =
                        '<div style="color: #ccc; margin: auto; font-size: 20px;">Loading gallery...</div>';

                    const url = link.href;
                    let images = cache.get(url);

                    if (!images) {
                        try {
                            const res = await fetch(url);
                            const text = await res.text();
                            const doc = new DOMParser().parseFromString(text, "text/html");

                            images = [];

                            // Strategy 1: Extract full-size images from .previewthumbs
                            // Structure: <div class="previewthumbs"><a href="...jp-N.jpg"><img src="...-N.jpg"></a>...</div>
                            // The <a> href contains the full-size image URL
                            const previewLinks = doc.querySelectorAll(".previewthumbs a[href]");
                            if (previewLinks.length > 0) {
                                previewLinks.forEach((a) => {
                                    const href = a.getAttribute("href") || "";
                                    if (href && (href.endsWith(".jpg") || href.endsWith(".png") || href.endsWith(".webp"))) {
                                        images.push(href);
                                    }
                                });
                            }

                            // Strategy 2: Look for the cover image
                            if (images.length === 0) {
                                const coverImg = doc.querySelector("#video_jacket_img, .video img[id]");
                                if (coverImg) {
                                    const coverSrc = coverImg.src || coverImg.getAttribute("src") || "";
                                    if (coverSrc) {
                                        images.push(coverSrc);
                                    }
                                }
                            }

                            // Strategy 3: Find all sample images by pattern
                            if (images.length === 0) {
                                const allImgs = doc.querySelectorAll("img");
                                allImgs.forEach((img) => {
                                    const src = img.src || img.getAttribute("src") || "";
                                    // DMM sample images typically contain "-" and end with "jp-" + number
                                    if (src.includes("pics.dmm.co.jp") && !src.includes("ps.jpg") && !src.includes("logo")) {
                                        images.push(src);
                                    }
                                });
                            }

                            // Strategy 4: Look for links to images
                            if (images.length === 0) {
                                const imgLinks = doc.querySelectorAll('a[href*=".jpg"], a[href*=".png"], a[href*=".webp"]');
                                imgLinks.forEach((a) => {
                                    if (a.href && !a.href.includes("logo")) {
                                        images.push(a.href);
                                    }
                                });
                            }

                            // Always try to add the cover as the first image
                            const coverImg = doc.querySelector("#video_jacket_img");
                            if (coverImg) {
                                const coverSrc = coverImg.src || coverImg.getAttribute("src") || "";
                                if (coverSrc && !images.includes(coverSrc)) {
                                    images.unshift(coverSrc);
                                }
                            }

                            if (images.length > 0) {
                                images = [...new Set(images)];
                                cache.set(url, images);
                            }
                        } catch (err) {
                            console.error("Gallery fetch error:", err);
                            content.innerHTML =
                                '<div style="color: red; margin: auto;">Failed to load gallery.</div>';
                            return;
                        }
                    }

                    // Render Images
                    if (images && images.length > 0) {
                        content.innerHTML = "";
                        images.forEach((imgUrl) => {
                            const img = document.createElement("img");
                            img.src = imgUrl;
                            Object.assign(img.style, {
                                maxWidth: "90vw",
                                maxHeight: "95vh",
                                width: "auto",
                                height: "auto",
                                objectFit: "contain",
                                borderRadius: "4px",
                                boxShadow: "0 4px 12px rgba(0,0,0,0.5)",
                                scrollSnapAlign: "center",
                                flexShrink: "0",
                                margin: "0 40px",
                            });
                            content.appendChild(img);
                        });
                    } else {
                        content.innerHTML =
                            '<div style="color: #aaa; margin: auto;">No images found in gallery section.</div>';
                    }
                });
                container.appendChild(galleryBtn);

                // --- Quick Search Buttons ---
                if (movieCode) {
                    const codeUpper = movieCode.toUpperCase();

                    // Nyaa
                    const nyaaBtn = document.createElement("a");
                    nyaaBtn.className = "javlib-tool-btn";
                    nyaaBtn.innerHTML = "N";
                    nyaaBtn.title = `Search ${codeUpper} on Nyaa`;
                    nyaaBtn.href = `https://sukebei.nyaa.si/?f=0&c=0_0&q=${codeUpper}`;
                    nyaaBtn.target = "_blank";
                    nyaaBtn.addEventListener("click", (e) => e.stopPropagation());
                    container.appendChild(nyaaBtn);

                    // JavDatabase
                    const jdbBtn = document.createElement("a");
                    jdbBtn.className = "javlib-tool-btn";
                    jdbBtn.innerHTML = "D";
                    jdbBtn.title = `Search ${codeUpper} on JavDatabase`;
                    jdbBtn.href = `https://www.javdatabase.com/movies/${codeUpper.toLowerCase()}/`;
                    jdbBtn.target = "_blank";
                    jdbBtn.addEventListener("click", (e) => e.stopPropagation());
                    container.appendChild(jdbBtn);

                    // JavDB
                    const javdbBtn = document.createElement("a");
                    javdbBtn.className = "javlib-tool-btn";
                    javdbBtn.innerHTML = "J";
                    javdbBtn.title = `Search ${codeUpper} on JavDB`;
                    javdbBtn.href = `https://javdb.com/search?q=${codeUpper}&f=all`;
                    javdbBtn.target = "_blank";
                    javdbBtn.addEventListener("click", (e) => e.stopPropagation());
                    container.appendChild(javdbBtn);
                }

                card.appendChild(container);
            });
        }

        injectIcons();

        // Return function to re-run on scroll
        return injectIcons;
    }

    // --- init ---
    upgradeImages();
    applyGrid();
    createPanel();
    const runGalleryInjector = setupGalleryFeature();

    // re-upgrade on scroll/lazy load
    const handleScroll = debounce(() => {
        upgradeImages();
        if (runGalleryInjector) runGalleryInjector();
    }, 300);

    window.addEventListener("scroll", handleScroll);
    window.addEventListener("resize", applyGrid);
})();