您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Preview images linked in destiny.gg chat. NSFW blur, hide-link, sticky autoscroll, optional 4chan block. Stable de-duplication by (message,timestamp,href).
当前为
// ==UserScript== // @name DGG Auto Image Embed (09/07/25 stable-dedupe, no-optchain) // @namespace boofus // @description Preview images linked in destiny.gg chat. NSFW blur, hide-link, sticky autoscroll, optional 4chan block. Stable de-duplication by (message,timestamp,href). // @match https://www.destiny.gg/embed/chat* // @icon https://cdn.destiny.gg/2.49.0/emotes/6296cf7e8ccd0.png // @version 10.2.5 // @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== (function(){ 'use strict'; // NOTE: no 'g' flag — .test() must be stateless 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; var overlay; // ---------------- settings ---------------- function ConfigItem(keyName, defaultValue){ this.keyName = keyName; this.defaultValue = defaultValue; } var configItems = { BlurNSFW : new ConfigItem("BlurNSFW", true), HideLink : new ConfigItem("HideLink", true), Block4cdn: new ConfigItem("Block4cdn", true) }; function Config(items, prefix){ this._prefix = prefix; for (var key in items){ (function(self, key, item){ var priv = "#" + item.keyName; Object.defineProperty(self, key, { set: function(v){ self[priv] = v; unsafeWindow.localStorage.setItem(prefix + item.keyName, JSON.stringify(v)); }, get: function(){ if (typeof self[priv] === "undefined"){ var raw = unsafeWindow.localStorage.getItem(prefix + item.keyName); self[priv] = raw != null ? JSON.parse(raw) : item.defaultValue; } return self[priv]; } }); })(this, key, items[key]); } } var config = new Config(configItems, "img-util."); // ---------------- helpers ---------------- var shouldStickToBottom = true; function getChatEl(){ return unsafeWindow.document.getElementsByClassName("chat-lines")[0]; } function isAtBottom(el, threshold){ if (!threshold) threshold = 2; return (el.scrollHeight - el.scrollTop - el.clientHeight) <= threshold; } function scrollToBottom(el){ requestAnimationFrame(function(){ el.scrollTop = el.scrollHeight; }); } function waitForElm(selector){ return new Promise(function(resolve){ var found = unsafeWindow.document.querySelector(selector); if (found) return resolve(found); var obs = new MutationObserver(function(){ var el = unsafeWindow.document.querySelector(selector); if (el){ obs.disconnect(); resolve(el); } }); obs.observe(unsafeWindow.document.body, { childList:true, subtree:true }); }); } function addSettings(){ var area = document.querySelector("#chat-settings-form"); if (!area){ console.warn("[DGG Img Preview] settings form not found"); return; } var title = document.createElement("h4"); title.textContent = "D.GG Img Preview Settings"; function mk(labelText, keyName){ var wrap = document.createElement("div"); wrap.className = "form-group checkbox"; var label = document.createElement("label"); label.textContent = labelText; var input = document.createElement("input"); input.type = "checkbox"; input.name = keyName; input.checked = !!config[keyName]; input.addEventListener("change", function(){ config[keyName] = input.checked; }); label.prepend(input); wrap.appendChild(label); return wrap; } area.appendChild(title); area.appendChild(mk("Blur nsfw/nsfl images", "BlurNSFW")); area.appendChild(mk("Hide link after preview", "HideLink")); area.appendChild(mk("Disable 4chan (i.4cdn.org) auto-embeds", "Block4cdn")); console.log("[DGG Img Preview] Settings Added"); } // Find the message container (direct child of .chat-lines) function findMessageContainer(node){ var chatEl = getChatEl(); var cur = node; while (cur && cur.parentNode && cur.parentNode !== chatEl){ cur = cur.parentNode; } if (cur && cur.parentNode === chatEl) return cur; return node.parentNode || node; } // Build a stable key: timestamp + username (fallback to first chunk of text) function getMessageKey(container){ var ts = ""; var user = ""; var timeEl = null; if (container && container.querySelector){ timeEl = container.querySelector('time[data-unixtimestamp]'); } if (timeEl) ts = timeEl.getAttribute('data-unixtimestamp') || ""; if (container && container.getAttribute) user = container.getAttribute('data-username') || ""; if (!user && container && container.querySelector){ var uEl = container.querySelector('[data-username]'); if (uEl && uEl.getAttribute) user = uEl.getAttribute('data-username') || ""; } var fallback = ""; if (!ts && !user){ var txt = (container && container.textContent) ? container.textContent : ""; fallback = txt.slice(0, 64); } return ts + "|" + user + "|" + fallback; } function hasPreviewForHref(container, href){ if (!container || !container.querySelectorAll) return false; var imgs = container.querySelectorAll('img[data-dgg-preview-src], img.__dggPreview'); for (var i=0; i<imgs.length; i++){ var a = imgs[i].getAttribute('data-dgg-preview-src'); if (a === href) return true; } return false; } // --------- stable global de-dupe --------- var processedKeys = new Set(); var inFlight = new WeakSet(); function handleLink(el){ // Optional domain block if (config.Block4cdn && /https?:\/\/i\.4cdn\.org\//i.test(el.href)) return; // Direct image? if (!imageRegex.test(el.href)) return; var container = findMessageContainer(el); var msgKey = getMessageKey(container); var globalKey = msgKey + "|" + el.href; // If we've handled (message,href) already, skip if (processedKeys.has(globalKey)) return; processedKeys.add(globalKey); // If there is already a preview inside this message for this href, skip if (hasPreviewForHref(container, el.href)){ if (config.HideLink && el.parentNode) el.parentNode.removeChild(el); return; } // Link-level flags if (el.dataset && el.dataset.dggPreviewed === "1") return; if (inFlight.has(el)) return; if (el.dataset) el.dataset.dggPreviewed = "1"; inFlight.add(el); GM_xmlhttpRequest({ method: "GET", url: el.href, responseType: "blob", onload: function(res){ var reader = new FileReader(); reader.readAsDataURL(res.response); reader.onloadend = function(){ var base64 = reader.result; // Re-check just before append (covers very fast remounts) if (hasPreviewForHref(container, el.href)){ if (config.HideLink && el.parentNode) el.parentNode.removeChild(el); inFlight.delete(el); return; } var img = unsafeWindow.document.createElement("img"); img.src = base64; img.setAttribute("data-dgg-preview-src", el.href); img.className = "__dggPreview"; 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"; var blurred = false; if (config.BlurNSFW && ((el.className && el.className.indexOf("nsfw") !== -1) || (el.className && el.className.indexOf("nsfl") !== -1))){ img.style.filter = "blur(15px)"; blurred = true; } img.onclick = function(){ if (blurred){ img.style.filter = "blur(0px)"; blurred = false; } else { overlay.style.display = "flex"; var full = unsafeWindow.document.createElement("img"); full.src = base64; full.style.maxHeight = "70%"; full.style.maxWidth = "70%"; full.style.display = "block"; full.style.position = "relative"; overlay.appendChild(full); var open = document.createElement("a"); open.href = el.href; open.textContent = "Open Original"; open.target = "_blank"; open.style.marginTop = "5px"; open.style.color = "#999"; overlay.appendChild(open); } }; var chatEl = getChatEl(); function onReady(){ if (shouldStickToBottom){ requestAnimationFrame(function(){ chatEl.scrollTop = chatEl.scrollHeight; }); } } if (typeof img.decode === "function"){ img.decode().then(onReady).catch(onReady); } else { img.addEventListener("load", onReady, { once:true }); img.addEventListener("error", onReady, { once:true }); } if (el.parentNode){ el.parentNode.appendChild(img); if (config.HideLink) el.parentNode.removeChild(el); } inFlight.delete(el); }; } }); } var chatObserver = new MutationObserver(function(mutations){ for (var mi=0; mi<mutations.length; mi++){ var m = mutations[mi]; for (var ni=0; ni<m.addedNodes.length; ni++){ var n = m.addedNodes[ni]; if (!n || n.nodeType !== 1) continue; // ELEMENT_NODE if (!n.querySelectorAll) continue; // Only unprocessed anchors (data flag avoids immediate re-hits) var links = n.querySelectorAll('.externallink:not([data-dgg-previewed="1"])'); for (var i=0; i<links.length; i++){ handleLink(links[i]); } } } }); // ---- bootstrap ---- console.log("[DGG Img Preview] Connecting"); waitForElm(".chat-lines").then(function(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 = function(){ overlay.style.display = "none"; overlay.innerHTML = ""; }; document.body.appendChild(overlay); // Sticky-bottom engine shouldStickToBottom = isAtBottom(chatEl); chatEl.addEventListener("scroll", function(){ shouldStickToBottom = isAtBottom(chatEl); }, { passive:true }); var ro = new ResizeObserver(function(){ if (shouldStickToBottom) scrollToBottom(chatEl); }); ro.observe(chatEl); 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(); }); })();