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.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==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);
})();