// ==UserScript== // @name Discord CustomBadges // @description Fetches badges from custom-badges.shadow-164.workers.dev with Discord-native tooltips, popup cards, and a full in-page dashboard for editing/publishing your own badge. // @version 12.2.0 // @match https://discord.com/* // @match https://*.discord.com/* // @icon https://files.catbox.moe/i4wx32.png // @grant none // @run-at document-start // @updateURL https://raw.githubusercontent.com/ItzMeShadow999/CustomBadgesUS/main/CustomBadges.userscript.js // @downloadURL https://raw.githubusercontent.com/ItzMeShadow999/CustomBadgesUS/main/CustomBadges.userscript.js // ==/UserScript== (function() { "use strict"; const API_BASE = "https://custom-badges.shadow-164.workers.dev"; const CACHE_TTL = 3e4; const badgeCache = new Map; const MARKER = "data-cb-remote-v11"; function log(...a) { console.log("[CustomBadges]", ...a); } const styleId = "cb-remote-styles"; if (!document.getElementById(styleId)) { const css = document.createElement("style"); css.id = styleId; css.textContent = `\n .cb-badge-img {\n object-fit: contain;\n display: inline-block;\n vertical-align: middle;\n cursor: default;\n }\n .cb-badge-img.cb-hover-scale {\n transition: transform 0.12s ease;\n }\n .cb-badge-img.cb-hover-glow {\n transition: filter 0.18s cubic-bezier(0.16,1,0.3,1);\n }\n .cb-badge-img.cb-clickable {\n cursor: pointer;\n }\n \n .cb-tooltip-el {\n position: fixed;\n z-index: 10002;\n pointer-events: none;\n opacity: 0;\n transform: translateY(4px);\n transition: opacity 120ms ease, transform 120ms ease;\n background: #000000;\n color: #ffffff;\n font-family: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;\n font-size: 14px;\n font-weight: 500;\n line-height: 18px;\n padding: 8px 12px;\n border-radius: 6px;\n box-shadow: 0 8px 16px rgba(0,0,0,0.36);\n white-space: nowrap;\n max-width: 280px;\n text-align: center;\n }\n .cb-tooltip-el.visible {\n opacity: 1;\n transform: translateY(0);\n }\n \n .cb-badge-popup {\n position: fixed;\n z-index: 10001;\n background: var(--cb-popup-bg, #1d1d1d);\n border-radius: 8px;\n padding: 20px 28px;\n text-align: center;\n box-shadow: 0 8px 24px rgba(0,0,0,0.5);\n opacity: 0;\n pointer-events: none;\n transition: opacity 0.24s cubic-bezier(0.16,1,0.3,1),\n transform 0.24s cubic-bezier(0.16,1,0.3,1);\n font-family: "gg sans", "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;\n min-width: 180px;\n width: fit-content;\n }\n .cb-badge-popup.visible {\n opacity: 1;\n pointer-events: auto;\n }\n \n .cb-badge-popup.cb-anim-fade {\n transform: translateY(8px) scale(0.96);\n }\n .cb-badge-popup.cb-anim-fade.visible {\n transform: translateY(0) scale(1);\n }\n .cb-badge-popup.cb-anim-scale {\n transform: scale(0.8);\n transform-origin: 50% 100%;\n }\n .cb-badge-popup.cb-anim-scale.visible {\n transform: scale(1);\n }\n .cb-badge-popup.cb-anim-slide {\n transform: translateY(16px);\n }\n .cb-badge-popup.cb-anim-slide.visible {\n transform: translateY(0);\n }\n \n .cb-badge-popup::after {\n content: "";\n position: absolute;\n top: 100%;\n left: 50%;\n transform: translateX(-50%);\n border-width: 7px;\n border-style: solid;\n border-color: var(--cb-popup-arrow, var(--cb-popup-bg, #1d1d1d)) transparent transparent transparent;\n }\n .cb-badge-popup img {\n width: 64px;\n height: 64px;\n border-radius: 50%;\n object-fit: cover;\n margin: 0 auto 14px auto;\n display: block;\n }\n .cb-badge-popup .cb-name {\n font-weight: 800;\n font-size: 16px;\n letter-spacing: 0.3px;\n line-height: 1.2;\n }\n .cb-badge-popup .cb-by {\n font-size: 12px;\n color: #949ba4;\n margin-top: 4px;\n }\n `; (document.head || document.documentElement).appendChild(css); } let _tooltipEl = null; function getTooltip() { if (_tooltipEl) return _tooltipEl; _tooltipEl = document.createElement("div"); _tooltipEl.className = "cb-tooltip-el"; document.body.appendChild(_tooltipEl); return _tooltipEl; } function showTooltip(text, rect) { const el = getTooltip(); el.textContent = text; el.classList.add("visible"); const ttRect = el.getBoundingClientRect(); let left = rect.left + rect.width / 2 - ttRect.width / 2; let top = rect.top - ttRect.height - 10; left = Math.max(8, Math.min(left, window.innerWidth - ttRect.width - 8)); top = Math.max(8, top); el.style.left = left + "px"; el.style.top = top + "px"; } function hideTooltip() { _tooltipEl?.classList.remove("visible"); } let _popupEl = null; let _popupOpenFor = null; let _followRaf = null; let _globalCloseAttached = false; let _onGlobalPointerDown = null; let _onGlobalScroll = null; function getPopupEl() { if (_popupEl) return _popupEl; _popupEl = document.createElement("div"); _popupEl.className = "cb-badge-popup"; document.body.appendChild(_popupEl); return _popupEl; } function positionPopup(target, el) { const rect = target.getBoundingClientRect(); const top = rect.top - el.offsetHeight - 12; const left = rect.left + rect.width / 2 - el.offsetWidth / 2; el.style.top = `${Math.max(top, 4)}px`; el.style.left = `${Math.max(left, 4)}px`; } function isTargetVisible(target) { if (!document.body.contains(target)) return false; const rect = target.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return false; const vw = window.innerWidth; const vh = window.innerHeight; if (rect.bottom <= 0 || rect.right <= 0 || rect.top >= vh || rect.left >= vw) return false; let node = target.parentElement; while (node && node !== document.body && node !== document.documentElement) { const cs = getComputedStyle(node); if (cs.overflow !== "visible" || cs.overflowX !== "visible" || cs.overflowY !== "visible") { const nr = node.getBoundingClientRect(); if (nr.width <= 0 || nr.height <= 0) return false; if (rect.bottom <= nr.top || rect.top >= nr.bottom || rect.right <= nr.left || rect.left >= nr.right) return false; } node = node.parentElement; } return true; } function startFollowingPopup(target) { stopFollowingPopup(); const step = () => { if (!_popupOpenFor || _popupOpenFor !== target) return; if (!isTargetVisible(target)) { hidePopup(); return; } positionPopup(target, getPopupEl()); _followRaf = requestAnimationFrame(step); }; _followRaf = requestAnimationFrame(step); } function stopFollowingPopup() { if (_followRaf != null) { cancelAnimationFrame(_followRaf); _followRaf = null; } } function hidePopup() { stopFollowingPopup(); detachGlobalCloseListeners(); getPopupEl().classList.remove("visible"); _popupOpenFor = null; } function attachGlobalCloseListeners() { if (_globalCloseAttached) return; _globalCloseAttached = true; _onGlobalPointerDown = e => { const popupEl = _popupEl; const openTarget = _popupOpenFor; if (!openTarget) return; if (popupEl && popupEl.contains(e.target)) return; if (e.target === openTarget || openTarget.contains && openTarget.contains(e.target)) return; hidePopup(); }; _onGlobalScroll = () => { hidePopup(); }; document.addEventListener("pointerdown", _onGlobalPointerDown, true); document.addEventListener("scroll", _onGlobalScroll, true); } function detachGlobalCloseListeners() { if (!_globalCloseAttached) return; _globalCloseAttached = false; if (_onGlobalPointerDown) document.removeEventListener("pointerdown", _onGlobalPointerDown, true); if (_onGlobalScroll) document.removeEventListener("scroll", _onGlobalScroll, true); _onGlobalPointerDown = null; _onGlobalScroll = null; } const _sampledColorCache = new Map; function sampleImageColor(url) { if (_sampledColorCache.has(url)) return Promise.resolve(_sampledColorCache.get(url)); return new Promise(resolve => { const img = new Image; img.crossOrigin = "anonymous"; img.onload = () => { try { const size = 32; const canvas = document.createElement("canvas"); canvas.width = size; canvas.height = size; const ctx = canvas.getContext("2d"); if (!ctx) return finish(null); ctx.drawImage(img, 0, 0, size, size); const data = ctx.getImageData(0, 0, size, size).data; let r = 0, g = 0, b = 0, count = 0; for (let i = 0; i < data.length; i += 4) { if (data[i + 3] < 32) continue; r += data[i]; g += data[i + 1]; b += data[i + 2]; count++; } if (!count) return finish(null); finish(`rgb(${Math.round(r / count)}, ${Math.round(g / count)}, ${Math.round(b / count)})`); } catch (e) { finish(null); } }; img.onerror = () => finish(null); img.src = url; function finish(color) { _sampledColorCache.set(url, color); resolve(color); } }); } async function getPopupBackground(imageUrl, style) { if (style.popupBackgroundMode === "edit") { return { background: `radial-gradient(120% 100% at 50% 0%, ${style.popupGradientSecondary} 0%, ${style.popupGradientMain} 65%)`, edgeColor: style.popupGradientMain }; } if (style.popupBackgroundMode === "sample") { const sampled = await sampleImageColor(imageUrl); if (sampled) { return { background: `radial-gradient(120% 100% at 50% 0%, ${sampled} 0%, #1d1d1d 65%)`, edgeColor: "#1d1d1d" }; } return { background: "#1d1d1d", edgeColor: "#1d1d1d" }; } return { background: "#1d1d1d", edgeColor: "#1d1d1d" }; } async function showBadgePopup(target, imageUrl, rawName, ownerUsername, style) { const el = getPopupEl(); const anim = style.popupAnimation || "fade"; el.className = `cb-badge-popup cb-anim-${anim}`; const displayName = formatBadgeName(rawName, style.appendTag); const nameColor = style.nameColor || "#ffffff"; const byLine = ownerUsername ? `