您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Shows a preview of images linked in destiny.gg chat without opening new tabs. NSFW blur toggle + preserves autoscroll + optional 4chan block.
当前为
// ==UserScript== // @name DGG Auto Image Embed (Release 09/07/25) // @namespace boofus // @description Shows a preview of images linked in destiny.gg chat without opening new tabs. NSFW blur toggle + preserves autoscroll + optional 4chan block. // @match https://www.destiny.gg/embed/chat* // @icon https://cdn.destiny.gg/2.49.0/emotes/6296cf7e8ccd0.png // @version 10.2.1 // @author boofus (credits: legolas, vyneer) // @license MIT // @grant GM_xmlhttpRequest // @run-at document-end // @connect discordapp.net // @connect discordapp.com // @connect pbs.twimg.com // @connect polecat.me // @connect imgur.com // @connect i.imgur.com // @connect gyazo.com // @connect redd.it // @connect i.redd.it // @connect catbox.moe // @connect i.4cdn.org // ==/UserScript== /** * Match direct-image links from common hosts. * NOTE: No 'g' flag — .test() should be stateless per call. */ const imageRegex = /https?:\/\/(?:i\.redd\.it|redd\.it|pbs\.twimg\.com|(?:media|cdn)\.discordapp\.(?:net|com)|i\.imgur\.com|imgur\.com|gyazo\.com|polecat\.me|catbox\.moe|i\.4cdn\.org)\/[^\s"'<>]+?\.(?:png|jpe?g|gif|gifv|webp)(?:\?[^\s"'<>]*)?$/i; let overlay; // ---------------- Settings (stolen spirit from vyneer/legolas) ---------------- class ConfigItem { constructor(keyName, defaultValue) { this.keyName = keyName; this.defaultValue = defaultValue; } } const configItems = { BlurNSFW : new ConfigItem("BlurNSFW", true), HideLink : new ConfigItem("HideLink", true), Block4cdn: new ConfigItem("Block4cdn", true), // default block i.4cdn.org embeds }; class Config { #configItems; #configKeyPrefix; constructor(configKeys, keyPrefix) { this.#configItems = configKeys; this.#configKeyPrefix = keyPrefix; for (const key in this.#configItems) { const configKey = this.#configItems[key]; const keyName = configKey.keyName; const privateKeyName = `#${keyName}`; Object.defineProperty(this, key, { set: function (value) { this[privateKeyName] = value; this.#save(keyName, value); }, get: function () { if (this[privateKeyName] === undefined) { this[privateKeyName] = this.#load(keyName) ?? configKey.defaultValue; } return this[privateKeyName]; }, }); } } #getFullKeyName(configKey) { return `${this.#configKeyPrefix}${configKey}`; } #save(configKey, value) { const fullKeyName = this.#getFullKeyName(configKey); unsafeWindow.localStorage.setItem(fullKeyName, JSON.stringify(value)); } #load(configKey) { const fullKeyName = this.#getFullKeyName(configKey); const item = unsafeWindow.localStorage.getItem(fullKeyName); if (item != null) return JSON.parse(item); } } const config = new Config(configItems, "img-util."); // sticky-bottom state shared with image handler let shouldStickToBottom = true; function getChatEl() { return unsafeWindow.document.getElementsByClassName("chat-lines")[0]; } function isAtBottom(el, threshold = 2) { return (el.scrollHeight - el.scrollTop - el.clientHeight) <= threshold; } function scrollToBottom(el) { requestAnimationFrame(() => { el.scrollTop = el.scrollHeight; }); } function addSettings() { const settingsArea = document.querySelector("#chat-settings-form"); if (!settingsArea) { console.warn("[DGG Img Preview] settings form not found"); return; } // Title const settingsTitle = document.createElement("h4"); settingsTitle.textContent = "D.GG Img Preview Settings"; // Helper to add a checkbox row const addCheckbox = (labelText, keyName) => { const wrap = document.createElement("div"); wrap.className = "form-group checkbox"; const label = document.createElement("label"); label.textContent = labelText; const input = document.createElement("input"); input.type = "checkbox"; input.name = keyName; input.checked = !!config[keyName]; input.addEventListener("change", () => { config[keyName] = input.checked; }); label.prepend(input); wrap.appendChild(label); return wrap; }; const blurNsfw = addCheckbox("Blur nsfw/nsfl images", "BlurNSFW"); const hideLink = addCheckbox("Hide link after preview", "HideLink"); const block4 = addCheckbox("Disable 4chan (i.4cdn.org) auto-embeds", "Block4cdn"); settingsArea.appendChild(settingsTitle); settingsArea.appendChild(blurNsfw); settingsArea.appendChild(hideLink); settingsArea.appendChild(block4); console.log("[DGG Img Preview] Settings Added"); } // ---------------- utilities ---------------- function waitForElm(selector) { // classic SO helper return new Promise(resolve => { const found = unsafeWindow.document.querySelector(selector); if (found) return resolve(found); const observer = new MutationObserver(() => { const el = unsafeWindow.document.querySelector(selector); if (el) { observer.disconnect(); resolve(el); } }); observer.observe(unsafeWindow.document.body, { childList: true, subtree: true }); }); } // ---------------- core logic ---------------- function handleLink(el) { // Optional block if (config.Block4cdn && /https?:\/\/i\.4cdn\.org\//i.test(el.href)) return; // Stateless test if (!imageRegex.test(el.href)) return; GM_xmlhttpRequest({ method: "GET", url: el.href, responseType: "blob", onload: function(res) { const reader = new FileReader(); reader.readAsDataURL(res.response); reader.onloadend = function() { const base64data = reader.result; const img = unsafeWindow.document.createElement("img"); img.src = base64data; img.style.maxHeight = "300px"; img.style.maxWidth = "300px"; img.style.marginLeft = "5px"; img.style.marginBottom = "10px"; img.style.marginTop = "10px"; img.style.display = "block"; img.style.cursor = "pointer"; // Blur for nsfw/nsfl links (class present on anchor) let blurred = false; if (config.BlurNSFW && (el.className.includes("nsfw") || el.className.includes("nsfl"))) { img.style.filter = "blur(15px)"; blurred = true; } img.onclick = () => { if (blurred) { img.style.filter = "blur(0px)"; blurred = false; } else { overlay.style.display = "flex"; const full = unsafeWindow.document.createElement("img"); full.src = base64data; full.style.maxHeight = "70%"; full.style.maxWidth = "70%"; full.style.display = "block"; full.style.position = "relative"; overlay.appendChild(full); const openOriginal = document.createElement("a"); openOriginal.href = el.href; openOriginal.textContent = "Open Original"; openOriginal.target = "_blank"; openOriginal.style.marginTop = "5px"; openOriginal.style.color = "#999"; overlay.appendChild(openOriginal); } }; // Append once, then fix scroll after decode/load const chatEl = getChatEl(); const onReady = () => { if (shouldStickToBottom) { requestAnimationFrame(() => { chatEl.scrollTop = chatEl.scrollHeight; }); } }; if ("decode" in img && typeof img.decode === "function") { img.decode().then(onReady).catch(onReady); } else { img.addEventListener("load", onReady, { once: true }); img.addEventListener("error", onReady, { once: true }); } el.parentNode.appendChild(img); if (config.HideLink) el.remove(); }; } }); } const chatObserver = new MutationObserver((mutations) => { for (const m of mutations) { for (const n of m.addedNodes) { if (n.nodeType !== 1) continue; // ELEMENT_NODE if (!n.querySelector) continue; if (!n.querySelector(".externallink")) continue; // No trailing space in selector const links = n.querySelectorAll(".externallink"); links.forEach(handleLink); } } }); console.log("[DGG Img Preview] Connecting"); waitForElm(".chat-lines").then((chatEl) => { // Overlay overlay = document.createElement("div"); overlay.style.position = "fixed"; overlay.style.justifyContent = "center"; overlay.style.alignItems = "center"; overlay.style.inset = "0px"; overlay.style.margin = "0px"; overlay.style.background = "rgba(0,0,0,0.85)"; overlay.style.zIndex = "999"; overlay.style.height = "100%"; overlay.style.width = "100%"; overlay.style.display = "none"; overlay.style.flexDirection = "column"; overlay.onclick = () => { overlay.style.display = "none"; overlay.innerHTML = ""; }; document.body.appendChild(overlay); // Sticky-bottom engine shouldStickToBottom = isAtBottom(chatEl); chatEl.addEventListener("scroll", () => { shouldStickToBottom = isAtBottom(chatEl); }, { passive: true }); const ro = new ResizeObserver(() => { if (shouldStickToBottom) scrollToBottom(chatEl); }); ro.observe(chatEl); // Start observing chat for new messages chatObserver.observe(chatEl, { attributes: false, childList: true, characterData: false, subtree: true }); console.log("[DGG Img Preview] Connected"); console.log("[DGG Img Preview] Adding Settings"); addSettings(); });