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