Sleazy Fork is available in English.
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.
// ==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);
})();