您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Replaces missing card images on arkhamdb. Supports card pages, modals and tooltips.
当前为
// ==UserScript== // @name ArkhamDB: replace missing images // @namespace Violentmonkey Scripts // @match https://arkhamdb.com/** // @grant none // @version 1.1.3 // @author @hiflix // @run-at document-end // @license MIT // @description Replaces missing card images on arkhamdb. Supports card pages, modals and tooltips. // ==/UserScript== const IMAGE_BASE_PATH = "https://assets.arkham.build/optimized"; /** * Handles most places where card images are displayed (including tooltips and modals). * known limitations: * - might break at any point without warning. * - doesn't handle double-sided cards (e.g. acts). * - `reviews` and `faq` might not work in all cases. */ function init() { tryReplaceBrokenImages(); replaceFullCards(); replaceCardPage(); replaceScansOnly(); replaceBrokenInvestigators(); watchCardTooltip(); watchCardModal(); } /** * Generic handler for places where arkhamdb tries to render a broken image. * This might fail due to race conditions with the loading of the site. */ function tryReplaceBrokenImages() { const url = window.location.href; // ignore set pages: we have specialized logic for these. if (!isSetPage(url)) { document.querySelectorAll("img").forEach((node) => { if (node.onerror) { node.onerror = "this.dataset.replace = true"; } }); window.addEventListener( "error", (evt) => { if (evt.target instanceof HTMLImageElement) { const match = /.*\/cards\/(.*)\.(?:png|jpg)/.exec(evt.target.src); if ( match?.length === 2 && !evt.target.src.startsWith(IMAGE_BASE_PATH) ) { evt.target.src = imageUrl(match[1]); } } }, true ); } } /** * Replace the "no image" placeholder on card pages with an image. * CAVEAT: does not handle double-sided cards. */ function replaceCardPage() { const url = window.location.href; const node = document.querySelector(".no-image"); if (!isSetPage(url) && node) { const id = idFromCardUrl(url); const image = htmlFromString( `<img class="img-responsive img-vertical-card" src="${imageUrl(id)}" />` ); node.parentNode.replaceChild(image, node); } } /** * Replace empty images on the "scans only" page. */ function replaceScansOnly() { const url = window.location.href; if (isSetPage(url) && (url.includes("/scan") || url.includes("/find"))) { document.querySelectorAll("a[href*='/card/'] > img").forEach((image) => { // check for empty image "src" attributes, which default to the current url. if (image.src === window.location.href) { const id = idFromCardUrl(image.parentNode.getAttribute("href")); image.src = imageUrl(id); } }); } } /** * Replace the "no image" placeholders on the "full cards" page. */ function replaceFullCards() { const url = window.location.href; if (isSetPage(url) && (url.includes("/card") || url.includes("/find"))) { document.querySelectorAll(".no-image").forEach((node) => { const id = node.parentNode?.parentNode?.previousElementSibling?.querySelector( "[data-code]" )?.dataset.code; if (id) { const image = htmlFromString( `<img loading="lazy" class="img-responsive img-vertical-card" src="${imageUrl( id )}" />` ); node.parentNode.appendChild(image); node.remove(); } }); } } /** * Replaces missing FHV investigators in the deck builder / deck list. */ function replaceBrokenInvestigators() { const REGEX_FHV_INVESTIGATORS = /.*\/cards\/(10.*)\.(?:png|jpg)/; const url = window.location.href; if (url.includes("/deck/") || url.includes("/decks")) { document .querySelectorAll(".deck-list-investigator-image") .forEach((node) => { const match = REGEX_FHV_INVESTIGATORS.exec( node.style["background-image"] ); if (match && match.length == 2) { node.style["background-image"] = `url(${imageUrl(match[1])})`; } }); } if (url.endsWith("/decklists")) { document.querySelectorAll(".decklist-faction-image img").forEach((node) => { const match = REGEX_FHV_INVESTIGATORS.exec(node.src); if (match && match.length == 2) { node.src = imageUrl(match[1]); } }); } if (!window.location.pathname || window.location.pathname === "/") { document .querySelectorAll(".card-thumbnail-investigator") .forEach((node) => { const match = REGEX_FHV_INVESTIGATORS.exec( node.style["background-image"] ); if (match && match.length == 2) { node.style["background-image"] = `url(${imageUrl(match[1])})`; } }); } } /** * Watch for the card modal opening, then replace the broken image inside. */ function watchCardModal() { const modal = document.querySelector("#cardModal"); if (!modal) return; // watch for images being added to the modal DOM. // filter for images that have src "undefined". const observer = new MutationObserver((mutationList) => { const image = mutationList.reduce( (acc, { type, addedNodes }) => acc || type !== "childList" ? acc : Array.from(addedNodes).find((node) => node?.src?.includes("undefined") ), undefined ); // traverse modal DOM to find card link and update image src. if (image) { const cardUrl = image .closest("#cardModal") .querySelector("a[href*='/card/']") .getAttribute("href"); image.src = imageUrl(idFromCardUrl(cardUrl)); } }); observer.observe(modal, { attributes: false, characterData: false, childList: true, subtree: true, }); } function watchCardTooltip() { const observer = new MutationObserver((mutationList) => { // watch for changes on links with a tooltip. // once changed, find the corresponding tooltip and update it. for (const { target, type } of mutationList) { if ( type === "attributes" && target instanceof HTMLAnchorElement && target.dataset.hasqtip ) { const tooltipId = target.dataset.hasqtip; const id = idFromCardUrl(target.href) || target?.dataset.code; const tooltip = document.querySelector(`#qtip-${tooltipId}-content`); if (tooltip && !tooltip.querySelector(".card-thumbnail")) { const url = imageUrl(id); // different card types art cropped differently, try to infer card type from tooltip text. const cardType = tooltip .querySelector(".card-type") ?.textContent?.split(".") ?.at(0) ?.toLowerCase() || "skill"; const image = htmlFromString(` <div class="card-thumbnail card-thumbnail-3x card-thumbnail-${cardType}" style="background-image: url(${url})" /> `); tooltip.prepend(image); } } } }); observer.observe(document.body, { attributes: true, childList: false, characterData: false, subtree: true, }); } function imageUrl(id) { return `${IMAGE_BASE_PATH}/${id}.webp`; } function idFromCardUrl(href) { return href.split("/").at(-1); } function isSetPage(url) { return ( url.includes("/set/") || url.includes("/cycle/") || url.includes("/find") ); } function htmlFromString(html) { const template = document.createElement("template"); html = html.trim(); template.innerHTML = html; return template.content.firstChild; } init();