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