Skip to content

Instantly share code, notes, and snippets.

@sdstpanda
Last active September 13, 2025 06:44
Show Gist options
  • Select an option

  • Save sdstpanda/97eb6c749bcbfed61f2cbd4a9da6cd35 to your computer and use it in GitHub Desktop.

Select an option

Save sdstpanda/97eb6c749bcbfed61f2cbd4a9da6cd35 to your computer and use it in GitHub Desktop.
VNDB Cover Preview
// ==UserScript==
// @name VNDB Cover Preview
// @namespace https://twitter.com/Kuroonehalf
// @namespace https://kuroonehalf.com
// @include https://vndb.org*
// @include https://vndb.org/v*
// @include https://vndb.org/g*
// @include https://vndb.org/p*
// @include https://vndb.org/u*
// @include https://vndb.org/s*
// @include https://vndb.org/r*
// @include https://vndb.org/c*
// @include https://vndb.org/t*
// @version 3.1.0
// @description Previews covers in vndb.org searches when hovering over the respective hyperlinks.
// @grant GM_setValue
// @grant GM_getValue
// @license http://creativecommons.org/licenses/by-nc-sa/4.0/
// ==/UserScript==
// ORIGINAL SCRIPT URL: https://greasyfork.org/en/scripts/5212-vndb-cover-preview
// --- Configuration ---
const HOVER_TIMEOUT = 200; // Delay in milliseconds before showing the preview
const BOLD_LINK = false; //set to true to make the hovered link bold temporarily
// ------
const apiPath = "https://api.vndb.org/kana/"; // API endpoint
// Disable native browser tooltips on links to prevent them from interfering
document.querySelectorAll("[title]").forEach(function (el) {
el.addEventListener("mouseover", function () {
// Store the title in a data attribute and remove the original
el.dataset.title = el.getAttribute("title");
el.removeAttribute("title");
});
el.addEventListener("mouseout", function () {
// Restore the title when the mouse leaves
if (el.dataset.title) {
el.setAttribute("title", el.dataset.title);
}
});
});
// Improved image positioning function: always near cursor, never under, and stays in viewport
function positionPopoverNearCursor(mouseEvent) {
const popover = document.getElementById("popover");
const img = popover.querySelector("img");
if (!img) return;
const padding = 24; // px, distance from cursor
const offsetX = 24; // px, horizontal offset from cursor
const offsetY = 8; // px, vertical offset from cursor
// Get viewport info
const winWidth = window.innerWidth;
const winHeight = window.innerHeight;
// Get image size
const imgRect = img.getBoundingClientRect();
const imgWidth = imgRect.width;
const imgHeight = imgRect.height;
// Cursor position relative to viewport
let x = mouseEvent.clientX;
let y = mouseEvent.clientY;
// Default position: right and below the cursor
let left = x + offsetX;
let top = y + offsetY;
// If popup would go off right edge, move to left of cursor
if (left + imgWidth + padding > winWidth) {
left = x - imgWidth - offsetX;
if (left < padding) left = winWidth - imgWidth - padding;
}
// If popup would go off bottom edge, move above cursor
if (top + imgHeight + padding > winHeight) {
top = y - imgHeight - offsetY;
if (top < padding) top = winHeight - imgHeight - padding;
}
// Set position relative to the viewport (for fixed positioning)
popover.style.left = left + "px";
popover.style.top = top + "px";
popover.classList.add("visible"); // Show only after positioned
}
// Add the popover element to the page
const popoverDiv = document.createElement("div");
popoverDiv.id = "popover";
document.body.appendChild(popoverDiv);
// Add popover and image styles using addMyStyle
const coverCSS = `
#popover {
position: fixed;
z-index: 99999;
opacity: 0;
pointer-events: none;
display: flex;
flex-direction: column;
align-items: flex-start;
/* transition: opacity 50ms ease-in-out; */
}
#popover.visible {
opacity: 1;
}
#popover .popover-title {
border: 1px solid var(--secborder);
border-bottom: none;
border-radius: 4px 4px 0 0;
background: var(--secbg);
color: var(--link);
padding: 2px 4px;
font-size: 1em;
text-align: start;
box-shadow: none;
max-width: 256px;
min-width: 0;
width: fit-content;
word-break: break-word;
display: block;
line-height: 1.4em;
position: relative;
z-index: 2;
align-self: flex-start;
}
#popover img {
display: block;
box-shadow: 0px 0px 5px black;
border-radius: 0 0 4px 4px;
max-width: 256px;
max-height: 360px;
width: auto;
height: auto;
background: var(--secbg);
}
`;
function addMyStyle(styleID, styleCSS) {
const myStyle = document.createElement("style");
myStyle.id = styleID;
myStyle.textContent = styleCSS;
document.head.appendChild(myStyle);
}
addMyStyle("cover-preview-style", coverCSS);
// Add mouseover and mouseleave event listeners to all table row links
document.querySelectorAll("tr a").forEach(function (link) {
let hoverTimer; // A timer specific to this link
// Function to display the image in the popover
async function showImage(url, mouseEvent, titleText) {
popoverDiv.classList.remove("visible"); // Hide before changing content
popoverDiv.innerHTML = "";
// Create a flex column wrapper for title and image
const wrapper = document.createElement("div");
wrapper.style.display = "flex";
wrapper.style.flexDirection = "column";
wrapper.style.alignItems = "flex-start";
// Create image
const img = document.createElement("img");
img.src = url;
// Create title div
if (titleText) {
const titleDiv = document.createElement("div");
titleDiv.className = "popover-title";
titleDiv.textContent = titleText;
// After image loads, set title width to match image width and position the popover
img.onload = function () {
titleDiv.style.width = img.offsetWidth + "px";
if (mouseEvent) {
positionPopoverNearCursor(mouseEvent);
}
};
wrapper.appendChild(titleDiv);
} else {
img.onload = function () {
if (mouseEvent) {
positionPopoverNearCursor(mouseEvent);
}
};
}
wrapper.appendChild(img);
popoverDiv.appendChild(wrapper);
}
// --- MOUSEOVER: Start a timer to fetch and show the image ---
link.addEventListener("mouseover", function (e) {
// Start a timer. The preview will only show if the mouse is still here after the timeout.
hoverTimer = setTimeout(async () => {
if (BOLD_LINK) {
link.style.fontWeight = "bold";
}
const VNnumber = link.getAttribute("href").substring(1);
const pagelink = "https://vndb.org/" + VNnumber;
const requestBody = JSON.stringify({
filters: ["id", "=", VNnumber],
fields: "image.url",
});
const type = VNnumber.charAt(0);
const titleText = link.dataset.title || link.getAttribute("title") || "";
// Prioritize checking the cache
const cachedUrl = GM_getValue(pagelink);
if (cachedUrl) {
await showImage(cachedUrl, e, titleText);
return;
}
if (type === "v" || type === "c") {
const endpoint = type === "v" ? "vn" : "character";
try {
const response = await fetch(apiPath + endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: requestBody,
});
const data = await response.json();
if (data.results && data.results[0] && data.results[0].image && data.results[0].image.url) {
const imageUrl = data.results[0].image.url;
await showImage(imageUrl, e, titleText);
GM_setValue(pagelink, imageUrl); // Cache the result
}
} catch (err) {
// Silently fail if API request fails
}
}
}, HOVER_TIMEOUT);
});
// --- MOUSELEAVE: Clear the timer and hide the image ---
link.addEventListener("mouseleave", function () {
// Cancel the pending timer so the preview doesn't appear
clearTimeout(hoverTimer);
// Hide any currently visible preview by making it transparent
popoverDiv.classList.remove("visible");
if (BOLD_LINK) {
link.style.fontWeight = "";
}
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment