JAVDatabase Full Covers + Grid Control

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(У мене вже є менеджер скриптів, дайте мені встановити його!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         JAVDatabase Full Covers + Grid Control
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Improves thumbnail image quality and adjusts cards per row for javdatabase.com movies pages. Features: clearer full-size covers, custom grid (3-16 cards/row), saves settings, full-screen gallery view.
// @match        https://www.javdatabase.com/*
// @match        https://javdatabase.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 ---
  function upgradeImages() {
    document.querySelectorAll(".movie-cover-thumb img").forEach((img) => {
      const src = img.src;
      if (src.includes("/covers/thumb/") && src.endsWith("ps.webp")) {
        img.src = src
          .replace("/covers/thumb/", "/covers/full/")
          .replace("ps.webp", "pl.webp");
      }
    });
  }

  // --- Grid Control ---
  const KEY = "javdb_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 = "javdb-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 = `
    .col-md-3.col-lg-2.col-xxl-2.col-4 {
        flex: 0 0 ${cardWidth}% !important;
        max-width: ${cardWidth}% !important;
        padding: 0 4px !important;
    }

    /* CARD ratio 800x536 (3:2) */
    .card {
        margin-bottom: 12px !important;
        aspect-ratio: 800 / 536 !important; /* exact 800:536 */
        /* or use 1.494 ≈ 800/536 */
        position: relative; /* For gallery icon */
    }

    .movie-cover-thumb {
        height: 80% !important; /* image takes 80% of card */
    }

    .movie-cover-thumb img {
        width: 100% !important;
        height: 100% !important;
        object-fit: cover !important;
        border-radius: 6px 6px 0 0 !important;
    }

    /* Text content compact at bottom */
    .card-body > *:not(.movie-cover-thumb) {
        font-size: 0.8em !important;
        padding: 4px 8px 0 !important;
        line-height: 1.3 !important;
    }

    .pcard {
        font-size: 0.9em !important;
        margin-bottom: 4px !important;
    }

    /* Tool Button Group */
    .javdb-tool-group {
        position: absolute;
        top: 5px;
        right: 5px;
        display: flex;
        gap: 4px;
        z-index: 10;
        opacity: 0.6;
        transition: opacity 0.2s;
        display: none; /* Hide by default */
    }

    .card:hover .javdb-tool-group {
        display: flex; /* Show on hover */
        opacity: 1;
    }

    .javdb-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;
    }

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

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

    const panel = document.createElement("div");
    panel.id = "javdb-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="javdb-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="javdb-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(`javdb-row-${n}`)
        .addEventListener("click", () => {
          cardsPerRow = n;
          saveCfg(n);
          applyGrid();
          createPanel();
        });
    });

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

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

    if (!modal) {
      modal = document.createElement("div");
      modal.id = "javdb-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", // Center vertically
        backdropFilter: "blur(5px)",
      });

      // Inner container for images (Horizontal Scroll)
      content = document.createElement("div");
      content.id = "javdb-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", // Remove padding to allow full width
      });

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

      // 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) {
          // Reset hasDragged in next mousedown or allow it to be ignored here
          // But we need to ensure it doesn't block legitimate clicks later
          // It is reset in mousedown
          e.stopPropagation(); // Stop propagation just in case
          return;
        }
        if (e.target === modal || e.target === content) closeModal();
      });

      // --- Drag Listeners ---
      content.addEventListener("mousedown", (e) => {
        // Prevent dragging image
        if (e.target.tagName === "IMG") {
          e.preventDefault();
        }
        isDown = true;
        hasDragged = false; // Reset drag state
        content.style.cursor = "grabbing";
        content.style.scrollSnapType = "none"; // Disable snap while dragging
        content.style.scrollBehavior = "auto"; // Disable smooth scroll while dragging
        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"; // Re-enable snap
        content.style.scrollBehavior = "smooth";
      });

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

        // Note: click event fires after mouseup
      });

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

        // Only mark as dragged if moved significantly (e.g. > 5px) to avoid preventing clicks on micro-movements
        if (Math.abs(walk) > 5) {
          hasDragged = true;
        }

        content.scrollLeft = scrollLeft - walk;
      });

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

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

          // Disable snap/smooth only if not already disabled to improve performance
          if (content.style.scrollSnapType !== "none") {
            content.style.scrollSnapType = "none";
            content.style.scrollBehavior = "auto";
          }

          const scrollSpeed = 2.5; // Reduced speed for smoother feel
          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("javdb-gallery-content");
      // Re-select buttons if they exist, though we only create once
      const btns = modal.querySelectorAll("button");
      // Assuming order or classes, but simpler to just keep references if scope allowed.
      // For simplicity in this script structure, we rely on the closure or re-query if needed.
    }

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

    function scrollGallery(direction) {
      const scrollAmount = window.innerWidth * 0.8; // Scroll 80% of screen width
      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.javdb_gallery_cache || new Map();
    window.javdb_gallery_cache = cache;

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

        // Try to extract movie ID
        // 1. From href: https://www.javdatabase.com/movies/orecs-436/ -> orecs-436
        let movieCode = "";
        try {
          const parts = link.href.split("/").filter((p) => p);
          movieCode = parts[parts.length - 1];
        } catch (e) {
          console.error("Error extracting code", e);
        }

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

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

          // Show Modal with Loading
          modal.style.display = "flex";
          document.body.style.overflow = "hidden"; // Disable scroll
          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: Find by data-image-href (High Quality)
              const highResLinks = doc.querySelectorAll("a[data-image-href]");
              if (highResLinks.length > 0) {
                highResLinks.forEach((a) => {
                  let href = a.getAttribute("data-image-href");
                  if (href) {
                    href = href.trim();
                    // Remove quotes if present
                    if (
                      (href.startsWith('"') && href.endsWith('"')) ||
                      (href.startsWith("'") && href.endsWith("'"))
                    ) {
                      href = href.slice(1, -1);
                    }
                    images.push(href);
                  }
                });
              }

              // Strategy 2: If no data-image-href, try to find the container following "Images" header
              if (images.length === 0) {
                // Find h4 with text "Images"
                const headers = Array.from(doc.querySelectorAll("h4"));
                const imgHeader = headers.find((h) =>
                  h.textContent.includes("Images"),
                );

                if (imgHeader) {
                  // The container is usually the next sibling div
                  let container = imgHeader.nextElementSibling;
                  // Skip if next sibling is not a div or container
                  while (container && container.tagName !== "DIV") {
                    container = container.nextElementSibling;
                  }

                  if (container) {
                    const links = container.querySelectorAll("a");
                    links.forEach((a) => {
                      if (
                        a.href &&
                        (a.href.endsWith(".jpg") ||
                          a.href.endsWith(".png") ||
                          a.href.endsWith(".webp"))
                      ) {
                        images.push(a.href);
                      }
                    });

                    if (images.length === 0) {
                      const imgs = container.querySelectorAll("img");
                      imgs.forEach((img) => {
                        images.push(img.src);
                      });
                    }
                  }
                }
              }

              // Strategy 3: Fallback to old XPath
              if (images.length === 0) {
                const xpath = '//*[@id="main"]/div/div[1]/div[5]';
                const result = doc.evaluate(
                  xpath,
                  doc,
                  null,
                  XPathResult.FIRST_ORDERED_NODE_TYPE,
                  null,
                );
                const container = result.singleNodeValue;
                if (container) {
                  const links = container.querySelectorAll("a");
                  links.forEach((a) => {
                    if (
                      a.href &&
                      (a.href.endsWith(".jpg") ||
                        a.href.endsWith(".png") ||
                        a.href.endsWith(".webp"))
                    ) {
                      images.push(a.href);
                    }
                  });
                }
              }

              if (images.length > 0) {
                // Deduplicate
                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", // Spacing between images
              });
              content.appendChild(img);
            });
          } else {
            content.innerHTML =
              '<div style="color: #aaa; margin: auto;">No images found in gallery section.</div>';
          }
        });
        container.appendChild(galleryBtn);

        // --- Nyaa & JavDB Buttons ---
        if (movieCode) {
          const codeUpper = movieCode.toUpperCase();

          // Nyaa
          const nyaaBtn = document.createElement("a");
          nyaaBtn.className = "javdb-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);

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

          // JavLibrary
          const javLibBtn = document.createElement("a");
          javLibBtn.className = "javdb-tool-btn";
          javLibBtn.innerHTML = "L";
          javLibBtn.title = `Search ${codeUpper} on JavLibrary`;
          javLibBtn.href = `https://www.javlibrary.com/en/vl_searchbyid.php?keyword=${codeUpper}`;
          javLibBtn.target = "_blank";
          javLibBtn.addEventListener("click", (e) => e.stopPropagation());
          container.appendChild(javLibBtn);
        }

        card.appendChild(container);
      });
    }

    injectIcons();

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

  // --- Clean Layout ---
  function cleanLayout() {
    const main = document.getElementById("main");
    if (!main) return;
    // Get direct children with class 'row' to avoid selecting nested rows
    const rows = Array.from(main.children).filter((el) =>
      el.classList.contains("row"),
    );

    // User requested to remove the first and last 'row', keeping the middle one.
    // We'll execute this if we find at least 3 rows to be safe, or just indiscriminately remove first and last if that's the strict instruction.
    // Based on "there are 3 divs class='row'", I'll target that structure.
    if (rows.length >= 3) {
      rows[0].style.display = "none"; // Safely hide or remove
      rows[rows.length - 1].style.display = "none";
    }
  }

  // --- init ---
  cleanLayout(); // Clean layout immediately
  upgradeImages(); // Update images immediately
  applyGrid(); // Apply grid immediately
  createPanel(); // Create panel
  const runGalleryInjector = setupGalleryFeature(); // setup gallery

  // re-upgrade khi scroll/load lazy (nếu có)
  const handleScroll = debounce(() => {
    upgradeImages();
    if (runGalleryInjector) runGalleryInjector();
  }, 300);

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