Replace DMM thumbnails with their respective full cover images
// ==UserScript==
// @license MIT
// @name JavLibrary - Enlarge Thumbnails
// @namespace https://sleazyfork.org/en/scripts/584159-javlibrary-enlarge-thumbnails
// @version 1.5
// @description Replace DMM thumbnails with their respective full cover images
// @match *://www.javlibrary.com/*
// @match *://javlibrary.com/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function () {
'use strict';
// ====== CONFIG ======
// Minimum card width (px). Number of columns is calculated automatically based on screen width.
// Larger value = bigger cards & fewer columns, and vice versa.
const MIN_CARD_WIDTH = 560;
const GAP = 12; // gap between cards (px)
// Card text font size. Leave null to AUTO-match the sidebar's font-size.
// Set manually (e.g. '15px') to force a specific size.
const FONT_SIZE = null;
const FONT_SIZE_FALLBACK = '16px'; // used as fallback if sidebar font cannot be detected
// Maximum number of title lines shown (rest is clipped with ellipsis).
const TITLE_LINES = 2;
// ====================
/* ---------- 1. Swap image ps.jpg -> pl.jpg ---------- */
function upgradeImg(img) {
if (img.dataset.jlUpgraded) return;
const src = img.currentSrc || img.src || img.getAttribute('src') || '';
if (!/pics\.dmm\.co\.jp/i.test(src)) return;
const upgraded = src.replace(/ps(\.jpe?g)(\?.*)?$/i, 'pl$1$2');
if (upgraded === src) return; // not a ps.jpg thumbnail, skip
img.dataset.jlUpgraded = '1';
img.dataset.jlOriginal = src;
img.removeAttribute('width');
img.removeAttribute('height');
img.style.setProperty('width', '100%', 'important');
img.style.setProperty('height', 'auto', 'important');
img.style.setProperty('max-width', 'none', 'important');
img.style.setProperty('display', 'block', 'important');
// fallback: if pl.jpg returns 404, revert to original thumbnail
img.addEventListener('error', function onErr() {
if (img.dataset.jlReverted) return;
img.dataset.jlReverted = '1';
img.removeEventListener('error', onErr);
img.src = img.dataset.jlOriginal;
img.style.setProperty('width', 'auto', 'important');
img.style.setProperty('max-width', '100%', 'important');
});
img.src = upgraded;
}
/* ---------- 2. Layout: responsive grid + remove hardcoded heights ---------- */
function findContainer(cell) {
// find the ancestor whose direct children include >= 2 .video cards (that is the grid)
let el = cell.parentElement;
while (el && el.querySelectorAll(':scope > .video').length < 2) {
el = el.parentElement;
}
return el || cell.parentElement;
}
function fixLayout() {
const cells = document.querySelectorAll('.video');
if (!cells.length) return;
const container = findContainer(cells[0]);
if (container && !container.dataset.jlGrid) {
container.dataset.jlGrid = '1';
container.style.setProperty('display', 'grid', 'important');
container.style.setProperty(
'grid-template-columns',
`repeat(auto-fill, minmax(min(${MIN_CARD_WIDTH}px, 100%), 1fr))`,
'important'
);
container.style.setProperty('gap', GAP + 'px', 'important');
container.style.setProperty('align-items', 'start', 'important');
container.style.setProperty('height', 'auto', 'important');
container.style.setProperty('max-height', 'none', 'important');
container.style.setProperty('overflow', 'visible', 'important');
// remove hardcoded heights from ancestor elements
let p = container.parentElement;
for (let i = 0; i < 3 && p; i++, p = p.parentElement) {
p.style.setProperty('height', 'auto', 'important');
p.style.setProperty('max-height', 'none', 'important');
p.style.setProperty('overflow', 'visible', 'important');
}
}
cells.forEach((cell) => {
if (cell.dataset.jlCell) return;
cell.dataset.jlCell = '1';
cell.style.setProperty('float', 'none', 'important');
cell.style.setProperty('width', 'auto', 'important');
cell.style.setProperty('height', 'auto', 'important');
cell.style.setProperty('margin', '0', 'important');
cell.style.setProperty('overflow', 'visible', 'important');
});
}
/* ---------- 3. Font size + text width matching the image ---------- */
function detectSidebarFontSize() {
const labels = ['Home', 'New Comments', 'New Releases', 'New Entries',
'Most Wanted', 'Best Rated', 'Categories', 'Ranking',
'Directory', 'Best Reviews', 'Forum'];
for (const a of document.querySelectorAll('a')) {
if (labels.includes((a.textContent || '').trim())) {
return getComputedStyle(a).fontSize;
}
}
return null;
}
function applyTextStyle() {
const size = FONT_SIZE || detectSidebarFontSize() || FONT_SIZE_FALLBACK;
let st = document.getElementById('jl-font-style');
if (!st) {
st = document.createElement('style');
st.id = 'jl-font-style';
(document.head || document.documentElement).appendChild(st);
}
st.textContent = `
/* Card: flex column so text stretches to full image width */
div.video {
display: flex !important;
flex-direction: column !important;
align-items: stretch !important;
}
/* Text divs: fill the full card width (= image width) */
div.video .title,
div.video .id {
width: 100% !important;
max-width: 100% !important;
box-sizing: border-box !important;
}
/* Film ID: single line, ellipsis if too long */
div.video .id, div.video .id a {
font-size: ${size} !important;
line-height: 1.4 !important;
white-space: nowrap !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
display: block !important;
}
/* Title: max ${TITLE_LINES} lines, rest clipped */
div.video .title, div.video .title a {
font-size: ${size} !important;
line-height: 1.4 !important;
display: -webkit-box !important;
-webkit-line-clamp: ${TITLE_LINES} !important;
-webkit-box-orient: vertical !important;
overflow: hidden !important;
white-space: normal !important;
text-overflow: ellipsis !important;
}
`;
}
/* ---------- 4. Run ---------- */
function run(root) {
root.querySelectorAll('img').forEach(upgradeImg);
fixLayout();
applyTextStyle();
}
run(document);
let scheduled = false;
const mo = new MutationObserver((mutations) => {
for (const m of mutations) {
m.addedNodes.forEach((node) => {
if (node.nodeType !== 1) return;
if (node.tagName === 'IMG') upgradeImg(node);
else if (node.querySelectorAll) node.querySelectorAll('img').forEach(upgradeImg);
});
}
if (!scheduled) {
scheduled = true;
requestAnimationFrame(() => { scheduled = false; fixLayout(); applyTextStyle(); });
}
});
mo.observe(document.documentElement, { childList: true, subtree: true });
})();