Last active
September 13, 2025 06:44
-
-
Save sdstpanda/97eb6c749bcbfed61f2cbd4a9da6cd35 to your computer and use it in GitHub Desktop.
VNDB Cover Preview
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==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