// ==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 ? `
By ${ownerUsername}
` : ""; el.innerHTML = `\n ${displayName}\n
${displayName}
\n ${byLine}\n `; el.classList.add("visible"); _popupOpenFor = target; positionPopup(target, el); startFollowingPopup(target); attachGlobalCloseListeners(); const result = await getPopupBackground(imageUrl, style); if (_popupOpenFor === target) { el.style.setProperty("--cb-popup-bg", result.background); el.style.setProperty("--cb-popup-arrow", result.edgeColor); } } function getUserId(root) { const avatar = root.querySelector('img[src*="cdn.discordapp.com/avatars/"]'); if (avatar) { const m = avatar.src.match(/avatars\/(\d+)\//); if (m) return m[1]; } const el = root.querySelector("[data-user-id]"); if (el) return el.dataset.userId; return null; } function getOwnerUsername(root) { const exactUsernameEl = root.querySelector('[class*="userTagUsername"]'); if (exactUsernameEl && exactUsernameEl.textContent && exactUsernameEl.textContent.trim()) { const exact = sanitizeUsername(exactUsernameEl.textContent); if (exact) return exact; } const nameSelectors = [ '[class*="username"]', '[class*="userTag"]', '[class*="nameTag"]', '[class*="nickname"]', "h1" ]; for (const sel of nameSelectors) { const candidates = root.querySelectorAll(sel); for (const el of candidates) { if (!el || !el.textContent || !el.textContent.trim()) continue; const candidate = sanitizeUsername(el.textContent); if (isLikelyUsername(candidate)) return candidate; } } const avatar = root.querySelector('img[src*="cdn.discordapp.com/avatars/"]'); if (avatar && avatar.alt && avatar.alt.trim()) { const candidate = sanitizeUsername(avatar.alt); if (isLikelyUsername(candidate)) return candidate; } for (const sel of nameSelectors) { const el = root.querySelector(sel); if (el && el.textContent && el.textContent.trim()) return sanitizeUsername(el.textContent); } return null; } async function fetchBadge(userId) { if (!userId) return null; const cached = badgeCache.get(userId); if (cached && Date.now() - cached.time < CACHE_TTL) return cached.data; try { const res = await fetch(`${API_BASE}?userId=${encodeURIComponent(userId)}`, { credentials: "omit", cache: "no-store" }); if (res.status === 404) { badgeCache.set(userId, { data: null, time: Date.now() }); return null; } if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); badgeCache.set(userId, { data: data, time: Date.now() }); return data; } catch (e) { log("Fetch error:", e.message); return null; } } function makeBadge(data, userId, ownerUsername) { const style = data.style || {}; const shape = style.iconShape || "circle"; const hoverFx = style.hoverEffect || "none"; const glowColor = style.glowColor || "#ffffff"; const iconSize = style.iconSize || 22; const badgeTitle = data.description || "Custom Badge"; const img = document.createElement("img"); img.src = data.imageUrl; img.alt = badgeTitle; img.referrerPolicy = "no-referrer"; img.className = "cb-badge-img cb-injected-badge"; img.dataset.cbUserId = userId; if (ownerUsername) img.dataset.cbOwnerUsername = ownerUsername; if (hoverFx === "scale") img.classList.add("cb-hover-scale"); if (hoverFx === "glow") img.classList.add("cb-hover-glow"); if (data.link) img.classList.add("cb-clickable"); const radius = shape === "circle" ? "50%" : shape === "rounded" ? "6px" : "0"; Object.assign(img.style, { width: `${iconSize}px`, height: `${iconSize}px`, borderRadius: radius, marginLeft: "4px" }); if (hoverFx === "scale") { img.addEventListener("mouseenter", () => { img.style.transform = "scale(1.15)"; }); img.addEventListener("mouseleave", () => { img.style.transform = ""; }); } else if (hoverFx === "glow") { img.addEventListener("mouseenter", () => { img.style.filter = `drop-shadow(0 0 6px ${glowColor})`; }); img.addEventListener("mouseleave", () => { img.style.filter = ""; }); } img.addEventListener("mouseenter", () => { const rect = img.getBoundingClientRect(); showTooltip(badgeTitle, rect); }); img.addEventListener("mouseleave", hideTooltip); img.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); if (_popupOpenFor === img) { hidePopup(); } else { showBadgePopup(img, data.imageUrl, data.description || "Custom Badge", img.dataset.cbOwnerUsername || null, style); } }); return img; } async function process(root) { if (root.hasAttribute(MARKER)) return; const userId = getUserId(root); if (!userId) return; root.setAttribute(MARKER, "1"); const data = await fetchBadge(userId); if (!data || !data.imageUrl) { log("No remote badge for", userId); return; } let container = root.querySelector('div[aria-label="User Badges"]'); if (!container) { const nameEl = root.querySelector('h1, [class*="nameTag"], [class*="nickname"]'); if (!nameEl) return; container = document.createElement("div"); container.setAttribute("aria-label", "User Badges"); container.setAttribute("role", "group"); container.style.cssText = "display:flex;flex-wrap:wrap;align-items:center;gap:4px;margin:6px 0;"; const parent = nameEl.closest("div[class]") || nameEl.parentElement; if (parent && parent.nextSibling) { parent.parentNode.insertBefore(container, parent.nextSibling); } else { return; } } container.querySelectorAll(".cb-injected-badge").forEach(el => el.remove()); const ownerUsername = getOwnerUsername(root); container.appendChild(makeBadge(data, userId, ownerUsername)); log("Injected remote badge for", userId); } function scan() { const roots = new Set; [ '[class*="userProfileModalInner"]', '[class*="userProfileModal"]', '[class*="userPopoutInner"]', '[class*="userPopout"]', '[class*="profilePanel"]', '[class*="accountProfilePopoutWrapper"]', '[role="dialog"]' ].forEach(sel => document.querySelectorAll(sel).forEach(el => roots.add(el))); document.querySelectorAll('img[src*="avatars"]').forEach(img => { if (img.getBoundingClientRect().width >= 64) { const p = img.closest("[class]"); if (p) roots.add(p); } }); document.querySelectorAll('div[aria-label="User Badges"]').forEach(badgesEl => { let anc = badgesEl; for (let i = 0; i < 8 && anc.parentElement; i++) { anc = anc.parentElement; if (anc.querySelector('img[src*="cdn.discordapp.com/avatars/"]')) break; } roots.add(anc); }); roots.forEach(process); try { injectDirectoryBadge(); } catch (e) {} } function headerBarHtml() { return `\n
\n \n
\n \n \n
\n
\n \n
\n
\n `; } function dashboardHtml(presetLabels = []) { const icon = { toggle: ``, pencil: ``, eye: ``, bolt: ``, grid: ``, box: ``, check: ``, trash: ``, plus: ``, shield: ``, clock: `` }; return `\n \n
\n
\n
\n

\n Custom Badges\n

\n

\n Adds a self-hosted custom badge with hover tooltip and click-to-view popup card, visible to anyone else running this plugin.\n

\n
\n
\n
\n
\n
${icon.shield}
\n
Account Verification
\n
\n

\n Prove you own this Discord account so the server accepts badge changes as coming from you. No passwords or long-lived Discord tokens are ever stored - just a short-lived, revocable proof.\n

\n
\n \n \n
\n
\n
Session Token
\n

Paste the token shown after verifying your account here.

\n
\n \n
\n
\n
\n
\n
Your Discord User ID (optional)
\n

This script normally auto-detects your account from the bottom-left user panel, but if publishing does nothing or a preview shows "Not detected", paste your ID here as a manual fallback. Enable Developer Mode in Discord (Settings → Advanced), then right-click your own avatar/username anywhere and choose "Copy User ID".

\n \n
\n
\n
\n
\n
${icon.clock}
\n
Write Budget
\n
\n
\n
\n Loading…\n
\n
\n \n
\n
\n
\n
\n
${icon.pencil}
\n
Edit Active Badge
\n
\n \n Not published yet\n
\n
\n
\n
Api Base Url
\n \n
\n
\n
My Badge Image Url
\n \n
\n
\n
My Badge Name
\n \n
\n \n
\n
\n
\n
${icon.eye}
\n
Live Preview
\n
\n
\n
\n Set your badge image and name above to see a live preview\n
\n \n
\n
\n
\n
\n
${icon.bolt}
\n
Quick Actions
\n
\n
\n \n \n \n
\n
\n
Import Badge Code
\n \n
\n \n
\n
Selected Preset
\n
\n
\n ${presetLabels[0] ?? "No presets"}\n \n
\n
\n ${presetLabels.map((label, i) => `
${label}
`).join("\n ")}\n
\n
\n \n
\n \n
\n
\n
\n
${icon.grid}
\n
My Badges
\n
\n

\n Your saved badge slots. Click "Use" on any badge to make it active and publish it. Add a new slot to build another look - you can have up to 12.\n

\n
\n \n
\n
\n
\n
${icon.box}
\n
Badge Packs
\n
\n

\n Import a pack of badges from a raw GitHub URL, or export your current badges as a pack to share with others.\n

\n
\n
Import Pack from URL
\n

Raw GitHub URL to a badge pack JSON file (e.g. https://raw.githubusercontent.com/you/repo/main/packs/friend-group.json). Use the raw.githubusercontent.com link, not a github.com/blob/... page.

\n \n
\n
\n \n \n \n \n
\n
\n
\n
\n
${icon.toggle}
\n
Behavior
\n
\n
\n
\n
Show Tooltip
\n
Show a small tooltip when hovering a custom badge
\n
\n \n
\n
\n
\n
Show Popup
\n
Show a popup card when clicking a custom badge.
\n
\n \n
\n
\n
\n
Show Owner Tag
\n
Show "By {username}" underneath the badge name in the popup. Always shows the real Discord display name - not editable.
\n
\n \n
\n
\n
\n
Append Tag
\n
Add a [BD] suffix after your badge name. Seen by everyone who views your badge.
\n
\n \n
\n
\n
\n
Hide Own Badge
\n
Don't show my own badge to myself when viewing my own profile
\n
\n \n
\n
\n
\n
\n
\n
\n
${icon.eye}
\n
Icon Appearance
\n
\n
\n
Icon Shape
\n
\n \n \n \n
\n \n
\n
\n
Icon Size 22px
\n \n
\n
\n
Hover Effect
\n
\n \n \n \n
\n \n
\n
\n
Glow Color
\n
\n \n #FFFFFF\n
\n
\n
\n
\n
\n
${icon.box}
\n
Popup Card
\n
\n
\n
Background
\n
\n \n \n \n
\n \n
\n
\n
\n
\n
Main Color
\n
\n \n #1D1D1D\n
\n
\n
\n
Second Color
\n
\n \n #2A2A38\n
\n
\n
\n
\n
\n
Name Color
\n
\n \n #FFFFFF\n
\n
\n
\n
Popup Animation
\n
\n \n \n \n
\n \n
\n
\n
\n
\n
${icon.eye}
\n
Live Preview
\n
\n
\n
\n Set your badge image and name in the Custom Badges tab to see a live preview\n
\n \n
\n
\n
\n
\n
\n
\n
\n
\n \n
📦 Badge Pack Sharing Guidelines
\n
Before sharing a pack, make sure it meets these standards so everyone has a smooth experience importing it.
\n
Format
\n
Your pack must be a valid JSON file hosted on raw.githubusercontent.com - no other hosts are accepted by the importer. The structure should look like this:
\n {\n "version": 1,\n "badges": [\n "base64encodedcode",\n "base64encodedcode"\n ]\n }\n
Each entry in the badges array is a badge code generated by the Make Pack button in your dashboard.
\n
Pack Size
\n
ⓘ The importer only loads the first 6 badges from any pack. The Make Pack button exports up to 12 badges (your current plugin save limit). Technically packs can be as large as you want, but we recommend a minimum of 6 and a maximum of 10–15 for the best experience.
\n
Content Rules
\n
⚠️ Packs that break these rules will be removed without warning.
\n \n
How to Submit
\n
    \n
  1. Generate your pack JSON using the Make Pack (Copy JSON) button
  2. \n
  3. Push it to the packs repo as packs/your-pack-name.json in https://github.com/ItzMeShadow999/Badges
  4. \n
  5. Open a PR with a short description of the theme
  6. \n
\n
Tips for a Good Pack
\n \n
\n \n
\n
\n `; } const BUILTIN_PRESETS = [ { label: "Hypesquad Legacy", imageUrl: "https://files.catbox.moe/lreui6.png", name: "Hypesquad legacy ", style: { iconShape: "circle", iconSize: 22, hoverEffect: "glow", glowColor: "#5865F2", nameColor: "#5865F2", appendTag: false }, prefs: { showTooltip: true, hideOwnBadge: false } }, { label: "Minecraft Account", imageUrl: "https://i.pinimg.com/736x/82/b2/1f/82b21fe6d9166c673eed585a5fc38ef5.jpg", name: "Mincraft Account", style: { iconShape: "circle", iconSize: 22, hoverEffect: "glow", glowColor: "#f54e6d", nameColor: "#ffffff", appendTag: false }, prefs: { showTooltip: true, hideOwnBadge: false } }, { label: "Konata Haii", imageUrl: "https://files.catbox.moe/lri82r.gif", name: "konata haii", style: { iconShape: "circle", iconSize: 22, hoverEffect: "glow", glowColor: "#4955e3", nameColor: "#ffffff", appendTag: false }, prefs: { showTooltip: true, hideOwnBadge: false } }, { label: "Cat", imageUrl: "https://i.ibb.co/4gWjN4fN/5c3d6e5876ff2a6ea5372317c5a4fbd7-removebg-preview.png", name: "Cat", style: { iconShape: "circle", iconSize: 22, hoverEffect: "glow", glowColor: "#ffd6de", nameColor: "#ffffff", appendTag: false }, prefs: { showTooltip: true, hideOwnBadge: false } }, { label: "Verified Discord User", imageUrl: "https://files.catbox.moe/aodhtf.png", name: "Verified Discord User", style: { iconShape: "circle", iconSize: 30, hoverEffect: "scale", glowColor: "#0095ff", nameColor: "#ffffff", appendTag: false }, prefs: { showTooltip: true, hideOwnBadge: false } }, { label: "I like Vencord", imageUrl: "https://files.catbox.moe/g2sqaj.png", name: "I like Vencord", style: { iconShape: "square", iconSize: 22, hoverEffect: "glow", glowColor: "#FCC1CC", nameColor: "#ffffff", appendTag: true }, prefs: { showTooltip: true, hideOwnBadge: false } } ]; const MAX_BADGES = 12; const DEFAULT_PACKS_REPO_URL = "https://github.com/ItzMeShadow999/Badges"; const BADGE_EXPIRY_WARNING_DAYS = 14; const BADGE_HISTORY_LIMIT = 2; const BADGE_HISTORY_DEBOUNCE_MS = 3e3; const REQUEST_TIMEOUT_MS = 1e4; const RATE_LIMIT_WINDOW_MS = 1e4; const RATE_LIMIT_MAX_REQUESTS = 50; const OWNER_TAG_FORMAT = "By {username}"; function normalizeBadgeName(name) { return (name || "").trim().toLowerCase().replace(/\s+/g, " "); } const BLOCKED_BADGE_NAMES = new Set([ "discord", "discord mod", "staff", "discord developer", "discord active developer", "discord staff", "discord moderator", "discord employee", "discord team", "discord partner", "discord support", "certified moderator", "verified bot developer" ].map(normalizeBadgeName)); const BLOCKED_BADGE_NAME_MESSAGE = "That badge name isn't allowed - it impersonates an official Discord role/badge"; function isBlockedBadgeName(name) { if (!name) return false; return BLOCKED_BADGE_NAMES.has(normalizeBadgeName(name)); } const DEFAULT_BADGE_STYLE = { iconShape: "circle", iconSize: 22, hoverEffect: "none", glowColor: "#ffffff", nameColor: "#ffffff", appendTag: false, popupAnimation: "fade", popupBackgroundMode: "base", popupGradientMain: "#1d1d1d", popupGradientSecondary: "#2a2a38", firstUsedDate: "" }; const DEFAULT_BADGE_PREFS = { showTooltip: true, hideOwnBadge: false, showPopup: true, showOwnerTag: true }; const SETTINGS_KEY = "cb-dashboard-settings-v1"; const HISTORY_KEY = "cb-dashboard-history-v1"; const SELF_ID_KEY = "cb-dashboard-self-id"; const DEFAULTS = { apiBaseUrl: API_BASE, sessionToken: "", selfUserId: "", myBadgeImageUrl: "", myBadgeName: "", selectedPreset: "0", showTooltip: true, appendTag: false, badgeNameColor: "#ffffff", badgeIconSize: 22, badgeIconShape: "circle", badgeHoverEffect: "none", badgeGlowColor: "#ffffff", hideOwnBadge: false, myBadgesJson: "[]", myActiveBadgeId: "", importBadgeCode: "", importPackUrl: "", packRepoUrl: DEFAULT_PACKS_REPO_URL, showPopup: true, showOwnerTag: true, popupBackgroundMode: "base", popupGradientMain: "#1d1d1d", popupGradientSecondary: "#2a2a38", popupAnimationStyle: "fade", firstUsedDate: "", packGuidelinesShown: false }; let _settingsCache = null; function allSettings() { if (_settingsCache) return _settingsCache; try { const raw = localStorage.getItem(SETTINGS_KEY); _settingsCache = raw ? Object.assign({}, DEFAULTS, JSON.parse(raw)) : Object.assign({}, DEFAULTS); } catch (e) { _settingsCache = Object.assign({}, DEFAULTS); } if (!_settingsCache.firstUsedDate) _settingsCache.firstUsedDate = (new Date).toISOString(); return _settingsCache; } function persistSettings() { try { localStorage.setItem(SETTINGS_KEY, JSON.stringify(_settingsCache)); } catch (e) { log("Failed to save dashboard settings:", e); } } function getSetting(key) { const all = allSettings(); return all[key] === undefined ? DEFAULTS[key] : all[key]; } function setSetting(key, value) { const all = allSettings(); all[key] = value; persistSettings(); } const dashboardSettingsProxy = new Proxy({}, { get(_t, prop) { return getSetting(String(prop)); }, set(_t, prop, value) { setSetting(String(prop), value); return true; } }); function loadBadgeHistory() { try { const v = JSON.parse(localStorage.getItem(HISTORY_KEY) || "[]"); return Array.isArray(v) ? v : []; } catch (e) { return []; } } function saveBadgeHistory(history) { try { localStorage.setItem(HISTORY_KEY, JSON.stringify(history.slice(0, BADGE_HISTORY_LIMIT))); } catch (e) {} } let lastHistoryPushAt = 0; function pushBadgeHistory(snapshot) { const now = Date.now(); if (now - lastHistoryPushAt < BADGE_HISTORY_DEBOUNCE_MS) return; lastHistoryPushAt = now; const history = loadBadgeHistory(); history.unshift(snapshot); saveBadgeHistory(history); } function toast(message, opts) { const type = opts && opts.type || "info"; const colors = { success: "#23A55A", error: "#DA373C", info: "#5865F2" }; const el = document.createElement("div"); el.textContent = message; el.style.cssText = `position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(20px);z-index:10050;background:${colors[type] || colors.info};color:#fff;padding:10px 18px;border-radius:8px;font-family:"gg sans","Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;font-weight:600;box-shadow:0 8px 24px rgba(0,0,0,0.4);opacity:0;transition:opacity .18s ease, transform .18s ease;max-width:420px;text-align:center;`; document.body.appendChild(el); requestAnimationFrame(() => { el.style.opacity = "1"; el.style.transform = "translateX(-50%) translateY(0)"; }); setTimeout(() => { el.style.opacity = "0"; el.style.transform = "translateX(-50%) translateY(20px)"; setTimeout(() => el.remove(), 220); }, 3200); } const KNOWN_STATUS_SUFFIXES = [ "Online", "Idle", "Away", "Do Not Disturb", "Streaming", "Invisible", "Offline" ]; const USERNAME_TOKEN_RE = /^@?[a-z0-9._]{2,32}$/; const ACTIVITY_WORD_BLOCKLIST = new Set([ "played", "playing", "listening", "watching", "streaming", "competing", "spotify", "editing", "ago", "hr", "hrs", "min", "mins", "now", "since", "elapsed" ]); function isLikelyUsername(s) { if (!s) return false; const clean = s.replace(/^@/, ""); return USERNAME_TOKEN_RE.test(clean) && !ACTIVITY_WORD_BLOCKLIST.has(clean.toLowerCase()); } function sanitizeUsername(raw) { let out = (raw || "").trim(); for (const suffix of KNOWN_STATUS_SUFFIXES) { if (out.length > suffix.length && out.endsWith(suffix)) { out = out.slice(0, out.length - suffix.length).trim(); } } out = out.trim(); const bulletMatch = out.match(/^@?([a-z0-9._]{2,32})\s*(?:•|\u2022)/i); if (bulletMatch && isLikelyUsername(bulletMatch[1])) return bulletMatch[1].replace(/^@/, ""); if (isLikelyUsername(out)) return out.replace(/^@/, ""); const tokens = out.split(/\s+/).filter(Boolean); for (let i = tokens.length - 1; i >= 0; i--) { if (isLikelyUsername(tokens[i])) return tokens[i].replace(/^@/, ""); } const runs = out.match(/[a-z0-9._]{2,32}/g) || []; for (const run of runs) { if (isLikelyUsername(run)) return run; } return out; } function getCurrentUser() { const override = (getSetting("selfUserId") || "").trim(); if (/^\d{15,25}$/.test(override)) { return { id: override, username: "you" }; } let id = localStorage.getItem(SELF_ID_KEY) || null; let username = null; const exactUsernameEl = document.querySelector('[class*="userTagUsername"]'); if (exactUsernameEl && exactUsernameEl.textContent && exactUsernameEl.textContent.trim()) { username = sanitizeUsername(exactUsernameEl.textContent); } const avatarSelectors = [ '[class*="panels"] img[src*="/avatars/"]', '[class*="panels"] img[src*="/embed/avatars/"]', '[class*="avatarStack"] img[src*="/avatars/"]', '[class*="accountProfile"] img[src*="/avatars/"]', '[class*="panelWrapper"] img[src*="/avatars/"]', '[class*="statusBox"] img[src*="/avatars/"]', '[data-list-item-id*="account"] img[src*="/avatars/"]' ]; let panelAvatar = null; for (const sel of avatarSelectors) { panelAvatar = document.querySelector(sel); if (panelAvatar) break; } if (panelAvatar) { const m = panelAvatar.src.match(/avatars\/(\d+)\//); if (m) { id = m[1]; try { localStorage.setItem(SELF_ID_KEY, id); } catch (e) {} } if (!username && panelAvatar.alt && panelAvatar.alt.trim()) { username = sanitizeUsername(panelAvatar.alt); } } if (!username) { const nameSelectors = [ '[class*="panels"] [class*="userTag"]', '[class*="accountProfile"] [class*="userTag"]', '[class*="panels"] [class*="nameTag"]', '[class*="panels"] [class*="username"]', '[class*="accountProfile"] [class*="nameTag"]', '[class*="accountProfile"] [class*="username"]' ]; for (const sel of nameSelectors) { const nameEl = document.querySelector(sel); if (nameEl && nameEl.textContent && nameEl.textContent.trim()) { username = sanitizeUsername(nameEl.textContent); break; } } } if (!id) return null; return { id: id, username: username || "you" }; } function resolveBadgeStyle(remote, ownerFirstUsedDate) { const base = Object.assign({}, DEFAULT_BADGE_STYLE, remote || {}); if (remote && typeof remote.appendVencordTag === "boolean") base.appendTag = remote.appendVencordTag; if (!base.firstUsedDate && ownerFirstUsedDate) base.firstUsedDate = ownerFirstUsedDate; return base; } function getMyBadgeStyle() { return { iconShape: getSetting("badgeIconShape"), iconSize: getSetting("badgeIconSize"), hoverEffect: getSetting("badgeHoverEffect"), glowColor: getSetting("badgeGlowColor"), nameColor: getSetting("badgeNameColor"), appendVencordTag: getSetting("appendTag"), popupAnimation: getSetting("popupAnimationStyle"), popupBackgroundMode: getSetting("popupBackgroundMode"), popupGradientMain: getSetting("popupGradientMain"), popupGradientSecondary: getSetting("popupGradientSecondary"), firstUsedDate: getSetting("firstUsedDate") }; } function resolveBadgePrefs(remote) { return Object.assign({}, DEFAULT_BADGE_PREFS, remote || {}); } function getMyBadgePrefs() { return { showTooltip: getSetting("showTooltip"), hideOwnBadge: getSetting("hideOwnBadge"), showPopup: getSetting("showPopup"), showOwnerTag: getSetting("showOwnerTag") }; } function formatOwnerTag(ownerUsername) { if (!ownerUsername) return null; return OWNER_TAG_FORMAT.replace("{username}", ownerUsername); } function formatBadgeName(rawName, appendTag) { return appendTag ? `${rawName} [BD]` : rawName; } async function dashGetPopupBackground(imageUrl, style) { if (style.popupBackgroundMode === "edit") { return { background: `radial-gradient(120% 100% at 50% 0%, ${style.popupGradientSecondary} 0%, ${style.popupGradientMain} 65%)`, edgeColor: style.popupGradientMain, sampleFailed: false }; } 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", sampleFailed: false }; return { background: "#1d1d1d", edgeColor: "#1d1d1d", sampleFailed: true }; } return { background: "#1d1d1d", edgeColor: "#1d1d1d", sampleFailed: false }; } async function getDashboardPreviewData() { const imageUrl = getSetting("myBadgeImageUrl"); const name = getSetting("myBadgeName"); if (!imageUrl || !name) return null; const style = getMyBadgeStyle(); const me = getCurrentUser(); const ownerUsername = me && me.username || null; const displayName = formatBadgeName(name, style.appendTag); const ownerTag = getSetting("showOwnerTag") ? formatOwnerTag(ownerUsername) : null; const {background: background, sampleFailed: sampleFailed} = await dashGetPopupBackground(imageUrl, style); return { imageUrl: imageUrl, displayName: displayName, ownerTag: ownerTag, nameColor: style.nameColor, iconShape: style.iconShape, iconSize: style.iconSize, background: background, sampleFailed: sampleFailed }; } function genBadgeId() { return `${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`; } function getMyBadges() { try { const parsed = JSON.parse(getSetting("myBadgesJson") || "[]"); return Array.isArray(parsed) ? parsed : []; } catch (e) { return []; } } function setMyBadgesLocal(list) { setSetting("myBadgesJson", JSON.stringify(list.slice(0, MAX_BADGES))); } function getActiveBadgeId() { return getSetting("myActiveBadgeId") || null; } function findBadgeEntry(id) { if (!id) return undefined; return getMyBadges().find(b => b.id === id); } function encodeBadgeCode() { const imageUrl = getSetting("myBadgeImageUrl"); const name = getSetting("myBadgeName"); if (!imageUrl || !name) return null; const payload = { imageUrl: imageUrl, name: name, style: getMyBadgeStyle(), prefs: getMyBadgePrefs() }; return btoa(unescape(encodeURIComponent(JSON.stringify(payload)))); } function decodeBadgeCode(code) { return JSON.parse(decodeURIComponent(escape(atob(code.trim())))); } function shareMyBadge() { const code = encodeBadgeCode(); if (!code) { toast("Set your badge image and name first", { type: "error" }); return; } navigator.clipboard.writeText(code).then(() => toast("Badge code copied to clipboard", { type: "success" })).catch(() => toast("Couldn't copy to clipboard", { type: "error" })); } function validateBadgePayload(parsed) { if (!parsed || typeof parsed !== "object") return "That code isn't a valid badge, it didn't decode to an object"; if (typeof parsed.imageUrl !== "string" || !parsed.imageUrl.trim()) return "That code is missing a valid image URL"; if (typeof parsed.name !== "string" || !parsed.name.trim()) return "That code is missing a valid badge name"; if (isBlockedBadgeName(parsed.name)) return BLOCKED_BADGE_NAME_MESSAGE; if (parsed.style !== undefined && parsed.style !== null && (typeof parsed.style !== "object" || Array.isArray(parsed.style))) return "That code has an invalid style block"; if (parsed.prefs !== undefined && parsed.prefs !== null && (typeof parsed.prefs !== "object" || Array.isArray(parsed.prefs))) return "That code has an invalid preferences block"; return null; } let suppressPublishOnChange = false; function applyBadgeState(state) { const style = resolveBadgeStyle(state.style); const prefs = resolveBadgePrefs(state.prefs); suppressPublishOnChange = true; try { setSetting("myBadgeImageUrl", state.imageUrl); setSetting("myBadgeName", state.name); setSetting("badgeIconShape", style.iconShape); setSetting("badgeIconSize", style.iconSize); setSetting("badgeHoverEffect", style.hoverEffect); setSetting("badgeGlowColor", style.glowColor); setSetting("badgeNameColor", style.nameColor); setSetting("appendTag", style.appendTag); setSetting("popupAnimationStyle", style.popupAnimation); setSetting("popupBackgroundMode", style.popupBackgroundMode); setSetting("popupGradientMain", style.popupGradientMain); setSetting("popupGradientSecondary", style.popupGradientSecondary); setSetting("showTooltip", prefs.showTooltip); setSetting("hideOwnBadge", prefs.hideOwnBadge); setSetting("showPopup", prefs.showPopup); setSetting("showOwnerTag", prefs.showOwnerTag); } finally { suppressPublishOnChange = false; } scheduleAutoPublish(0); } function importBadgeFromCode() { const code = getSetting("importBadgeCode"); if (!code) { toast("Paste a badge code first", { type: "error" }); return; } let parsed; try { parsed = decodeBadgeCode(code); } catch (e) { toast("That badge code isn't valid base64/JSON", { type: "error" }); return; } const err = validateBadgePayload(parsed); if (err) { toast(err, { type: "error" }); return; } applyBadgeState({ imageUrl: parsed.imageUrl, name: parsed.name, style: parsed.style, prefs: parsed.prefs }); setSetting("importBadgeCode", ""); toast("Badge imported and saved", { type: "success" }); } function applySelectedPreset() { const preset = BUILTIN_PRESETS[Number(getSetting("selectedPreset"))]; if (!preset) { toast("Pick a preset first", { type: "error" }); return; } applyBadgeState({ imageUrl: preset.imageUrl, name: preset.name, style: preset.style, prefs: preset.prefs }); toast(`Applied "${preset.label}" preset and published`, { type: "success" }); } function revertBadge() { const history = loadBadgeHistory(); if (!history.length) { toast("No previous badge saved to revert to yet", { type: "error" }); return; } const previous = history.shift(); saveBadgeHistory(history); applyBadgeState(previous); toast("Reverted to your previous badge", { type: "success" }); } function apiBase() { return getSetting("apiBaseUrl") || API_BASE; } function taggedError(kind, detail) { return new Error(`${kind}:${detail}`); } async function fetchWithTimeout(url, options) { const controller = new AbortController; const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS); try { return await fetch(url, Object.assign({}, options, { signal: controller.signal })); } catch (e) { if (e && e.name === "AbortError") { let host = url; try { host = new URL(url).host; } catch (e2) {} throw taggedError("TIMEOUT", `Request to ${host} timed out`); } throw taggedError("NETWORK", e && e.message || "Network request failed"); } finally { clearTimeout(timeout); } } async function parseJsonOrThrow(res) { let data = null; try { data = await res.json(); } catch (e) {} if (res.status === 429) { const retryAfter = res.headers.get("Retry-After") ?? ""; throw taggedError("SERVER_RATE_LIMIT", retryAfter || data && data.error || "Too many requests"); } if (!res.ok) { throw taggedError("SERVER_ERROR", `${res.status}:${data && data.error || res.statusText || "Unknown error"}`); } return data; } let _writeRequestTimestamps = []; function checkClientRateLimit() { const timestamps = _writeRequestTimestamps; const now = Date.now(); while (timestamps.length && now - timestamps[0] > RATE_LIMIT_WINDOW_MS) timestamps.shift(); if (timestamps.length >= RATE_LIMIT_MAX_REQUESTS) { const retryAfterMs = RATE_LIMIT_WINDOW_MS - (now - timestamps[0]); throw taggedError("CLIENT_RATE_LIMIT", String(Math.max(retryAfterMs, 0))); } timestamps.push(now); } function authHeaders(sessionToken) { const headers = { "Content-Type": "application/json" }; if (sessionToken) headers.Authorization = `Bearer ${sessionToken}`; return headers; } function requireSessionToken(sessionToken) { if (!sessionToken) throw taggedError("NOT_VERIFIED", 'Verify your Discord account in settings first ("Verify Discord Account" button)'); return sessionToken; } async function apiSetBadge(userId, badgeId, imageUrl, description, style) { checkClientRateLimit(); const token = requireSessionToken(getSetting("sessionToken")); const res = await fetchWithTimeout(apiBase(), { method: "POST", headers: authHeaders(token), body: JSON.stringify({ action: "setBadge", userId: userId, badgeId: badgeId, imageUrl: imageUrl, description: description, style: style }) }); return parseJsonOrThrow(res); } async function apiSetActiveBadge(userId, badgeId) { checkClientRateLimit(); const token = requireSessionToken(getSetting("sessionToken")); const res = await fetchWithTimeout(apiBase(), { method: "POST", headers: authHeaders(token), body: JSON.stringify({ action: "setActiveBadge", userId: userId, badgeId: badgeId }) }); return parseJsonOrThrow(res); } async function apiDeleteBadge(userId, badgeId) { checkClientRateLimit(); const token = requireSessionToken(getSetting("sessionToken")); const res = await fetchWithTimeout(apiBase(), { method: "POST", headers: authHeaders(token), body: JSON.stringify({ action: "deleteBadge", userId: userId, badgeId: badgeId }) }); return parseJsonOrThrow(res); } async function apiRevokeToken() { const token = requireSessionToken(getSetting("sessionToken")); const res = await fetchWithTimeout(`${apiBase()}/self/revoke`, { method: "POST", headers: authHeaders(token), body: JSON.stringify({}) }); return parseJsonOrThrow(res); } async function apiGetWriteBudget() { const token = requireSessionToken(getSetting("sessionToken")); const res = await fetchWithTimeout(`${apiBase()}/self/writes`, { method: "GET", headers: authHeaders(token) }); return parseJsonOrThrow(res); } function describeBadgeApiError(e) { const message = e instanceof Error ? e.message : String(e); const sep = message.indexOf(":"); const kind = sep === -1 ? message : message.slice(0, sep); const detail = sep === -1 ? "" : message.slice(sep + 1); switch (kind) { case "NOT_VERIFIED": return 'Verify your Discord account first (see the "Verify Discord Account" button in settings)'; case "CLIENT_RATE_LIMIT": { const seconds = Math.max(1, Math.ceil(Number(detail) / 1e3) || 1); return `Slow down a little - try again in ${seconds}s`; } case "SERVER_RATE_LIMIT": return detail && /^\d+$/.test(detail) ? `Rate limited by the badge server - try again in ${detail}s` : "Rate limited by the badge server - try again shortly"; case "TIMEOUT": return "Request timed out - the badge server didn't respond in time"; case "NETWORK": return "Couldn't reach the badge server - check your connection"; case "SERVER_ERROR": return "The badge server had a problem - try again in a bit"; default: return "Something went wrong talking to the badge server"; } } function primeOwnBadgeCache(userId, apiRes) { if (!userId || !apiRes || !apiRes.ok) return false; const active = (apiRes.badges || []).find(b => b.id === apiRes.activeId) || (apiRes.badges || [])[0]; if (!active) return false; badgeCache.set(userId, { data: { firstRequestAt: apiRes.firstRequestAt, badges: apiRes.badges, activeId: apiRes.activeId, expiresAt: apiRes.expiresAt, imageUrl: active.imageUrl, description: active.description, ...active.style ? { style: active.style } : {} }, time: Date.now() }); return true; } function refreshOwnBadgeDisplay(userId, apiRes) { const primed = primeOwnBadgeCache(userId, apiRes); if (!primed && userId) badgeCache.delete(userId); document.querySelectorAll(`[${MARKER}]`).forEach(el => { if (!userId || getUserId(el) === userId) el.removeAttribute(MARKER); }); if (!primed) badgeCache.clear(); scan(); } let lastPublishedSnapshot = null; const AUTO_PUBLISH_DEBOUNCE_MS = 700; const AUTO_PUBLISH_RETRY_DELAYS_MS = [ 3e3, 8e3, 2e4, 45e3 ]; let _autoPublishTimer = null; let _autoPublishRetryTimer = null; let _autoPublishRetryCount = 0; let _autoPublishGeneration = 0; let _publishStatusListeners = []; function onPublishStatusChange(fn) { _publishStatusListeners.push(fn); return () => { _publishStatusListeners = _publishStatusListeners.filter(f => f !== fn); }; } function setPublishStatus(state, detail) { _publishStatusListeners.forEach(fn => { try { fn(state, detail); } catch (e) {} }); } function cancelPendingAutoPublish() { if (_autoPublishTimer) { clearTimeout(_autoPublishTimer); _autoPublishTimer = null; } if (_autoPublishRetryTimer) { clearTimeout(_autoPublishRetryTimer); _autoPublishRetryTimer = null; } _autoPublishRetryCount = 0; } function scheduleAutoPublish(delay = AUTO_PUBLISH_DEBOUNCE_MS) { cancelPendingAutoPublish(); const myGeneration = ++_autoPublishGeneration; setPublishStatus("pending"); _autoPublishTimer = setTimeout(() => { _autoPublishTimer = null; runAutoPublish(myGeneration); }, delay); } async function runAutoPublish(generation) { if (generation !== _autoPublishGeneration) return; const imageUrl = getSetting("myBadgeImageUrl"); const name = getSetting("myBadgeName"); if (!imageUrl || !name) { setPublishStatus("idle"); return; } if (isBlockedBadgeName(name)) { toast(BLOCKED_BADGE_NAME_MESSAGE, { type: "error" }); setPublishStatus("error", "Blocked badge name"); return; } setPublishStatus("publishing"); const ok = await updateMyBadgeFromSettings(); if (generation !== _autoPublishGeneration) return; if (ok) { _autoPublishRetryCount = 0; setPublishStatus("success"); } else { const delay = AUTO_PUBLISH_RETRY_DELAYS_MS[Math.min(_autoPublishRetryCount, AUTO_PUBLISH_RETRY_DELAYS_MS.length - 1)]; _autoPublishRetryCount++; setPublishStatus("error", `Retrying in ${Math.round(delay / 1e3)}s`); _autoPublishRetryTimer = setTimeout(() => { _autoPublishRetryTimer = null; runAutoPublish(generation); }, delay); } } async function setMyBadge(badgeId, imageUrl, description) { const me = getCurrentUser(); if (!me) { log("Not logged in / could not detect current user"); toast('Could not detect your Discord account - open the "Your Discord User ID" field in settings and paste it in manually', { type: "error" }); return false; } try { const res = await apiSetBadge(me.id, badgeId, imageUrl, description, getMyBadgeStyle()); refreshOwnBadgeDisplay(me.id, res); applyWriteBudget(res && res.writeBudget); log("Badge set:", res); if (res && res.ok && res.activeId && res.activeId !== badgeId) { setSetting("myActiveBadgeId", res.activeId); toast(`Saved, but your active badge is a different slot - switched local tracking to match. Use "Switch to badge" if you meant to edit ${badgeId === res.badgeId ? "this one" : "the one you just edited"}.`, { type: "error" }); } return true; } catch (e) { log("Failed to set badge:", e); toast(describeBadgeApiError(e), { type: "error" }); return false; } } async function updateMyBadgeFromSettings() { if (suppressPublishOnChange) return true; const imageUrl = getSetting("myBadgeImageUrl"); const name = getSetting("myBadgeName"); if (!imageUrl || !name) return true; if (isBlockedBadgeName(name)) { toast(BLOCKED_BADGE_NAME_MESSAGE, { type: "error" }); return false; } if (lastPublishedSnapshot) pushBadgeHistory(lastPublishedSnapshot); lastPublishedSnapshot = { imageUrl: imageUrl, name: name, style: getMyBadgeStyle(), prefs: getMyBadgePrefs() }; let id = getActiveBadgeId(); if (!id) { id = genBadgeId(); setSetting("myActiveBadgeId", id); } const list = getMyBadges(); const entry = { id: id, imageUrl: imageUrl, description: name, style: getMyBadgeStyle() }; const idx = list.findIndex(b => b.id === id); if (idx === -1) list.push(entry); else list[idx] = entry; setMyBadgesLocal(list); return await setMyBadge(id, imageUrl, name); } function loadBadgeFieldsIntoSettings(entry) { suppressPublishOnChange = true; try { setSetting("myBadgeImageUrl", entry.imageUrl); setSetting("myBadgeName", entry.description); const style = resolveBadgeStyle(entry.style); setSetting("badgeIconShape", style.iconShape); setSetting("badgeIconSize", style.iconSize); setSetting("badgeHoverEffect", style.hoverEffect); setSetting("badgeGlowColor", style.glowColor); setSetting("badgeNameColor", style.nameColor); setSetting("appendTag", style.appendTag); setSetting("popupAnimationStyle", style.popupAnimation); setSetting("popupBackgroundMode", style.popupBackgroundMode); setSetting("popupGradientMain", style.popupGradientMain); setSetting("popupGradientSecondary", style.popupGradientSecondary); } finally { suppressPublishOnChange = false; } } async function switchToBadge(id) { const entry = findBadgeEntry(id); if (!entry) return; setSetting("myActiveBadgeId", id); loadBadgeFieldsIntoSettings(entry); const me = getCurrentUser(); if (!me) { toast("Could not detect your Discord account - paste your User ID into settings manually", { type: "error" }); return; } try { const res = await apiSetActiveBadge(me.id, id); refreshOwnBadgeDisplay(me.id, res); applyWriteBudget(res && res.writeBudget); toast("Switched active badge", { type: "success" }); } catch (e) { log("Failed to switch active badge:", e); toast(describeBadgeApiError(e), { type: "error" }); } } function createNewBadgeSlot() { const list = getMyBadges(); if (list.length >= MAX_BADGES) { toast(`You can only have up to ${MAX_BADGES} badges`, { type: "error" }); return; } const id = genBadgeId(); const entry = { id: id, imageUrl: getSetting("myBadgeImageUrl") || "", description: getSetting("myBadgeName") || "New Badge", style: getMyBadgeStyle() }; list.push(entry); setMyBadgesLocal(list); setSetting("myActiveBadgeId", id); loadBadgeFieldsIntoSettings(entry); if (entry.imageUrl) scheduleAutoPublish(0); toast("New badge slot added - edit the fields above to customize it", { type: "success" }); } async function deleteBadgeSlot(id) { const list = getMyBadges(); const remaining = list.filter(b => b.id !== id); setMyBadgesLocal(remaining); const me = getCurrentUser(); if (me) { try { const res = await apiDeleteBadge(me.id, id); refreshOwnBadgeDisplay(me.id, res); applyWriteBudget(res && res.writeBudget); } catch (e) { log("Failed to delete badge:", e); toast(describeBadgeApiError(e), { type: "error" }); } } else { toast("Removed locally, but could not reach the server (Discord account not detected) - it may still show on your profile", { type: "error" }); } if (getActiveBadgeId() === id) { const next = remaining[0]; if (next) { switchToBadge(next.id); } else { setSetting("myActiveBadgeId", ""); suppressPublishOnChange = true; try { setSetting("myBadgeImageUrl", ""); setSetting("myBadgeName", ""); } finally { suppressPublishOnChange = false; } } } } function refreshBadgeCache() { badgeCache.clear(); toast("Badge cache cleared", { type: "success" }); } function verifyDiscordAccount() { window.open(`${apiBase()}/auth/start`, "_blank", "noopener,noreferrer"); } async function revokeSessionToken() { try { await apiRevokeToken(); setSetting("sessionToken", ""); _writeBudgetInfo = null; renderWriteBudget(); toast("Token revoked - re-verify to publish badge changes again", { type: "success" }); } catch (e) { log("Failed to revoke token:", e); toast(describeBadgeApiError(e), { type: "error" }); } } function packUrlLooksValid(url) { try { return new URL(url).hostname === "raw.githubusercontent.com"; } catch (e) { return false; } } function makePack() { const badges = getMyBadges(); if (!badges.length) { toast("You don't have any badges to pack yet", { type: "error" }); return; } const codes = badges.map(b => btoa(unescape(encodeURIComponent(JSON.stringify({ imageUrl: b.imageUrl, name: b.description, style: b.style }))))); const pack = { version: 1, badges: codes }; navigator.clipboard.writeText(JSON.stringify(pack, null, 2)).then(() => { const repoHint = getSetting("packRepoUrl") ? ` Push it to ${getSetting("packRepoUrl")} as packs/your-pack-name.json.` : ' Set "Pack Repo Url" below, then push this as packs/your-pack-name.json in that repo.'; toast(`Pack JSON copied to clipboard.${repoHint}`, { type: "success" }); }).catch(() => toast("Couldn't copy to clipboard", { type: "error" })); } function browsePacks() { window.open("https://github.com/ItzMeShadow999/Badges", "_blank", "noopener,noreferrer"); } async function importPackFromUrl() { const url = (getSetting("importPackUrl") || "").trim(); if (!url) { toast("Paste a pack URL first", { type: "error" }); return; } if (!packUrlLooksValid(url)) { toast("Use a raw.githubusercontent.com link, not a github.com/blob/... page", { type: "error" }); return; } let codes; try { const res = await fetch(url); if (!res.ok) throw new Error(res.statusText); const data = await res.json(); codes = Array.isArray(data) ? data : Array.isArray(data && data.badges) ? data.badges : []; if (!codes.length) throw new Error("empty pack"); } catch (e) { toast("Couldn't load that pack - check the URL", { type: "error" }); return; } const list = getMyBadges(); let imported = 0; for (const code of codes) { if (list.length + imported >= MAX_BADGES) break; try { const parsed = decodeBadgeCode(code); if (validateBadgePayload(parsed)) continue; list.push({ id: genBadgeId(), imageUrl: parsed.imageUrl, description: parsed.name, style: parsed.style }); imported++; } catch (e) {} } if (!imported) { toast("No valid badges found in that pack", { type: "error" }); return; } setMyBadgesLocal(list); for (const entry of list.slice(-imported)) { await setMyBadge(entry.id, entry.imageUrl, entry.description); } toast(`Imported ${imported} badge(s) from pack and saved`, { type: "success" }); } let _dashboardBridge = null; function setDashboardBridge(b) { _dashboardBridge = b; } function getDashboardBridge() { return _dashboardBridge; } let _writeBudgetInfo = null; let _writeBudgetCountdownTimer = null; function formatWriteBudgetDuration(ms) { if (ms <= 0) return "0m"; const totalMinutes = Math.ceil(ms / 6e4); const h = Math.floor(totalMinutes / 60); const m = totalMinutes % 60; return h > 0 ? `${h}h ${m}m` : `${m}m`; } function renderWriteBudget() { const countEl = document.getElementById("ub-write-budget-count"); const resetEl = document.getElementById("ub-write-budget-reset"); const cardEl = document.getElementById("ub-write-budget-card"); const fillEl = document.getElementById("ub-write-budget-bar-fill"); if (!countEl || !resetEl || !cardEl) return; if (!getSetting("sessionToken")) { countEl.textContent = "Verify your account to see your write budget"; resetEl.textContent = ""; cardEl.classList.remove("ub-write-budget-exhausted"); if (fillEl) fillEl.style.width = "100%"; return; } if (!_writeBudgetInfo) { countEl.textContent = "Loading…"; resetEl.textContent = ""; return; } const {remaining: remaining, limit: limit, resetAt: resetAt} = _writeBudgetInfo; const safeLimit = limit || 0; const exhausted = remaining <= 0; countEl.textContent = `${remaining} / ${safeLimit} writes remaining`; cardEl.classList.toggle("ub-write-budget-exhausted", exhausted); if (fillEl) fillEl.style.width = `${safeLimit > 0 ? Math.max(0, Math.min(100, remaining / safeLimit * 100)) : 100}%`; if (resetAt == null) { resetEl.textContent = ""; } else { const msLeft = resetAt - Date.now(); if (msLeft <= 0) { resetEl.textContent = "Refreshing…"; fetchWriteBudget(); } else { resetEl.textContent = `${exhausted ? "Available again" : "Resets"} in ${formatWriteBudgetDuration(msLeft)}`; } } } function applyWriteBudget(info) { if (!info) return; _writeBudgetInfo = { remaining: info.remaining, limit: info.limit, resetAt: info.resetAt ?? null }; renderWriteBudget(); } async function fetchWriteBudget() { if (!document.getElementById("ub-write-budget-count")) return; if (!getSetting("sessionToken")) { renderWriteBudget(); return; } try { const data = await apiGetWriteBudget(); applyWriteBudget(data); } catch (e) { log("Failed to fetch write budget:", e); } } function startWriteBudgetCountdown() { if (_writeBudgetCountdownTimer) return; _writeBudgetCountdownTimer = setInterval(() => { if (_writeBudgetInfo && _writeBudgetInfo.resetAt != null) renderWriteBudget(); }, 3e4); } function stopWriteBudgetCountdown() { if (_writeBudgetCountdownTimer) { clearInterval(_writeBudgetCountdownTimer); _writeBudgetCountdownTimer = null; } } let _dashboardActive = false; function setDashboardActive(active) { _dashboardActive = active; } let _dashboardOverlay = null; function closeDashboard() { setDashboardActive(false); if (_dashboardOverlay) _dashboardOverlay.style.display = "none"; stopWriteBudgetCountdown(); } function mountDashboard() { const overlay = document.createElement("div"); overlay.id = "ub-dashboard-content"; overlay.style.cssText = "position:fixed;inset:0;z-index:10010;display:none;flex-direction:column;background:#000;"; overlay.innerHTML = headerBarHtml() + dashboardHtml(BUILTIN_PRESETS.map(p => p.label)); document.body.appendChild(overlay); document.addEventListener("keydown", e => { if (e.key === "Escape" && _dashboardActive) closeDashboard(); }); wireDashboardSettings(overlay); document.addEventListener("visibilitychange", () => { if (!document.hidden && _dashboardActive) fetchWriteBudget(); }); _dashboardOverlay = overlay; return overlay; } function openDashboard() { setDashboardActive(true); const overlay = _dashboardOverlay || mountDashboard(); overlay.style.display = "flex"; renderWriteBudget(); fetchWriteBudget(); startWriteBudgetCountdown(); } function initDashboard() { setDashboardBridge({ settings: { store: dashboardSettingsProxy }, presetLabels: BUILTIN_PRESETS.map(p => p.label), getPreviewData: () => getDashboardPreviewData(), shareMyBadge: () => shareMyBadge(), revertBadge: () => revertBadge(), refreshBadgeCache: () => refreshBadgeCache(), importBadgeFromCode: () => importBadgeFromCode(), applySelectedPreset: () => applySelectedPreset(), createNewBadgeSlot: () => createNewBadgeSlot(), importPackFromUrl: () => importPackFromUrl(), makePack: () => makePack(), browsePacks: () => browsePacks(), onBadgeModeChange: () => {}, publishBadge: () => scheduleAutoPublish(), applyBadgeNow: () => scheduleAutoPublish(0), onPublishStatusChange: fn => onPublishStatusChange(fn), verifyAccount: () => verifyDiscordAccount(), revokeSessionToken: () => revokeSessionToken(), switchToBadge: id => switchToBadge(id), deleteBadgeSlot: id => deleteBadgeSlot(id) }); createDashboardFab(); } function createDashboardFab() { if (document.getElementById("cb-dashboard-fab")) return; const fab = document.createElement("button"); fab.id = "cb-dashboard-fab"; fab.type = "button"; fab.title = "Custom Badges Dashboard"; fab.innerHTML = ``; fab.style.cssText = "position:fixed;right:20px;bottom:20px;z-index:9999;width:48px;height:48px;border-radius:50%;background:#5865F2;color:#fff;border:none;box-shadow:0 4px 14px rgba(0,0,0,0.4);display:flex;align-items:center;justify-content:center;cursor:pointer;transition:transform .15s ease,background .15s ease;"; fab.addEventListener("mouseenter", () => { fab.style.background = "#4752C4"; fab.style.transform = "scale(1.06)"; }); fab.addEventListener("mouseleave", () => { fab.style.background = "#5865F2"; fab.style.transform = "scale(1)"; }); fab.addEventListener("click", () => openDashboard()); document.body.appendChild(fab); } function wireDashboardSettings(root) { const bridge = getDashboardBridge(); if (!bridge) { console.warn("[UserDashboard] Dashboard bridge not set yet - settings form will not be wired up."); return; } const {settings: settings} = bridge; const $ = id => root.querySelector(`#${id}`); const closeBtn = $("ub-dash-close"); const publishStatusDot = $("ub-publish-status-dot"); const publishStatusText = $("ub-publish-status-text"); if (bridge.onPublishStatusChange) { bridge.onPublishStatusChange((state, detail) => { if (!publishStatusDot || !publishStatusText) return; const map = { idle: { color: "var(--ub-text-faint)", text: "Not published yet" }, pending: { color: "var(--ub-warning)", text: "Waiting for you to stop typing…" }, publishing: { color: "var(--ub-accent-2)", text: "Sending to server…" }, success: { color: "var(--ub-positive)", text: "Synced to server ✓" }, error: { color: "var(--ub-danger)", text: detail || "Failed to sync - retrying" } }; const s = map[state] || map.idle; publishStatusDot.style.background = s.color; publishStatusText.textContent = s.text; publishStatusText.style.color = s.color; }); } if (closeBtn && closeBtn.getAttribute("data-ub-listener") !== "true") { closeBtn.setAttribute("data-ub-listener", "true"); closeBtn.addEventListener("click", () => { setDashboardActive(false); closeDashboard(); }); closeBtn.addEventListener("mouseenter", () => { closeBtn.style.background = "var(--background-modifier-hover,rgba(79,84,92,0.16))"; closeBtn.style.color = "#fff"; }); closeBtn.addEventListener("mouseleave", () => { closeBtn.style.background = "transparent"; closeBtn.style.color = "#949BA4"; }); } const apiBaseUrl = $("ub-api-base-url"); const badgeImageUrl = $("ub-badge-image-url"); const badgeName = $("ub-badge-name"); const importBadgeCode = $("ub-import-badge-code"); const importPackUrl = $("ub-import-pack-url"); const sessionToken = $("ub-session-token"); const selfUserId = $("ub-self-user-id"); const sessionTokenOverlay = $("ub-session-token-overlay-inner"); const revokeTokenBtn = $("ub-revoke-token"); const badgeModeInput = $("ub-badge-mode"); const selectedPresetInput = $("ub-selected-preset"); const iconSize = $("ub-icon-size"); const iconSizeValue = $("ub-icon-size-value"); const hoverEffectInput = $("ub-hover-effect"); const glowColorField = $("ub-glow-color-field"); const glowColor = $("ub-glow-color"); const glowColorHex = $("ub-glow-color-hex"); const bgModeInput = $("ub-bg-mode"); const gradientFields = $("ub-gradient-fields"); const gradientMain = $("ub-gradient-main"); const gradientMainHex = $("ub-gradient-main-hex"); const gradientSecondary = $("ub-gradient-secondary"); const gradientSecondaryHex = $("ub-gradient-secondary-hex"); const nameColor = $("ub-name-color"); const nameColorHex = $("ub-name-color-hex"); const showTooltipSwitch = $("ub-show-tooltip"); const showPopupSwitch = $("ub-show-popup"); const showPopupRow = $("ub-show-popup-row"); const showOwnerTagSwitch = $("ub-show-owner-tag"); const showOwnerTagRow = $("ub-show-owner-tag-row"); const appendTagSwitch = $("ub-append-tag"); const hideOwnBadgeSwitch = $("ub-hide-own-badge"); function wireDropdown(dropdownId, hiddenInputId, valueElId, onChange) { const dropdown = root.querySelector(`#${dropdownId}`); const hiddenInput = $(hiddenInputId); const valueEl = root.querySelector(`#${valueElId}`); const trigger = root.querySelector(`#${dropdownId} .ub-dropdown-trigger`); const menu = root.querySelector(`#${dropdownId} .ub-dropdown-menu`); if (!dropdown || !hiddenInput || !valueEl || !trigger || !menu) return; trigger.addEventListener("click", e => { e.stopPropagation(); const isOpen = dropdown.classList.contains("open"); root.querySelectorAll(".ub-dropdown.open").forEach(d => d.classList.remove("open")); if (!isOpen) dropdown.classList.add("open"); }); menu.querySelectorAll(".ub-dropdown-option").forEach(opt => { opt.addEventListener("click", () => { const val = opt.dataset.value ?? ""; hiddenInput.value = val; valueEl.textContent = opt.textContent ?? ""; menu.querySelectorAll(".ub-dropdown-option").forEach(o => o.classList.remove("selected")); opt.classList.add("selected"); dropdown.classList.remove("open"); onChange?.(val); }); }); } document.addEventListener("click", () => { root.querySelectorAll(".ub-dropdown.open").forEach(d => d.classList.remove("open")); }); const TAB_COPY = { badges: { title: "Custom Badges", subtitle: "Adds a self-hosted custom badge with hover tooltip and click-to-view popup card, visible to anyone else running this plugin." }, style: { title: "Styles Menu", subtitle: "Shape, size, hover effects, popup background, name color, and animation - everything that controls how your badge looks. Every change here is visible to anyone who views your badge." } }; const pageHeading = root.querySelector("#ub-page-heading"); const pageSubtitle = root.querySelector("#ub-page-subtitle"); const tabUnderline = root.querySelector("#ub-tab-underline"); const tabsContainer = root.querySelector("#ub-tabs-container"); let activeTab = "badges"; const prefersReducedMotion = window.matchMedia?.("(prefers-reduced-motion: reduce)").matches; function positionUnderline(tabEl) { if (!tabUnderline || !tabsContainer || !tabEl) return; const containerRect = tabsContainer.getBoundingClientRect(); const tabRect = tabEl.getBoundingClientRect(); tabUnderline.style.left = `${tabRect.left - containerRect.left}px`; tabUnderline.style.width = `${tabRect.width}px`; } function switchTab(tab) { if (tab === activeTab) return; activeTab = tab; root.querySelectorAll(".ub-dash-tab").forEach(t => { const active = t.dataset.tab === tab; t.setAttribute("aria-selected", String(active)); t.style.borderBottomColor = active ? "#5865F2" : "transparent"; t.style.color = active ? "#fff" : "#949BA4"; t.style.fontWeight = active ? "600" : "500"; if (active) positionUnderline(t); }); const copy = TAB_COPY[tab] ?? TAB_COPY.badges; if (pageHeading) pageHeading.textContent = copy.title; if (pageSubtitle) pageSubtitle.textContent = copy.subtitle; const targetPanel = root.querySelector(`#ub-panel-${tab}`); const currentPanel = root.querySelector(".ub-tabpanel:not(.ub-hidden)"); if (!targetPanel || targetPanel === currentPanel) return; if (prefersReducedMotion) { currentPanel?.classList.add("ub-hidden"); targetPanel.classList.remove("ub-hidden"); return; } currentPanel?.classList.add("ub-panel-fade-out"); setTimeout(() => { currentPanel?.classList.add("ub-hidden"); currentPanel?.classList.remove("ub-panel-fade-out"); targetPanel.classList.remove("ub-hidden"); targetPanel.classList.add("ub-panel-fade-in"); requestAnimationFrame(() => { requestAnimationFrame(() => targetPanel.classList.remove("ub-panel-fade-in")); }); }, 150); } root.querySelectorAll(".ub-dash-tab").forEach(t => { t.addEventListener("click", () => switchTab(t.dataset.tab ?? "badges")); }); positionUnderline(root.querySelector('.ub-dash-tab[aria-selected="true"]')); window.addEventListener("resize", () => { positionUnderline(root.querySelector(`#ub-tab-${activeTab}`)); }); function wireChoiceGroup(groupId, hiddenInputId, onChange) { const group = root.querySelector(`#${groupId}`); const hiddenInput = $(hiddenInputId); if (!group || !hiddenInput) return; group.querySelectorAll(".ub-choice").forEach(btn => { btn.addEventListener("click", () => { const val = btn.dataset.value ?? ""; hiddenInput.value = val; group.querySelectorAll(".ub-choice").forEach(b => b.classList.remove("selected")); btn.classList.add("selected"); onChange?.(val); }); }); } function setChoiceGroupValue(groupId, hiddenInputId, val) { const group = root.querySelector(`#${groupId}`); const hiddenInput = $(hiddenInputId); if (!group || !hiddenInput) return; hiddenInput.value = val; group.querySelectorAll(".ub-choice").forEach(b => { b.classList.toggle("selected", b.dataset.value === val); }); } function wireSwitch(btn, onChange) { if (!btn) return; btn.addEventListener("click", () => { if (btn.disabled) return; const next = !btn.classList.contains("on"); btn.classList.toggle("on", next); btn.setAttribute("aria-checked", String(next)); onChange(next); }); } function setSwitchValue(btn, val) { if (!btn) return; btn.classList.toggle("on", val); btn.setAttribute("aria-checked", String(val)); } let tokenMasked = !!settings.store.sessionToken; function escapeHtml(s) { return s.replace(/&/g, "&").replace(//g, ">"); } function syncOverlayScroll() { if (!sessionToken || !sessionTokenOverlay) return; sessionTokenOverlay.style.transform = `translateX(${-sessionToken.scrollLeft}px)`; } function buildTokenChars(masked) { if (!sessionToken || !sessionTokenOverlay) return; const value = sessionToken.value; sessionToken.classList.toggle("ub-token-empty", !value); if (!value) { sessionTokenOverlay.innerHTML = ""; syncOverlayScroll(); return; } const chars = value.split(""); sessionTokenOverlay.innerHTML = chars.map((ch, i) => { const delay = masked ? i * 16 : (chars.length - 1 - i) * 16; return `` + `${escapeHtml(ch)}` + `•` + ``; }).join(""); syncOverlayScroll(); } function setTokenMasked(masked) { if (!sessionTokenOverlay) return; const charEls = sessionTokenOverlay.querySelectorAll(".ub-token-char"); const total = charEls.length; charEls.forEach((charEl, i) => { const delay = masked ? i * 16 : (total - 1 - i) * 16; const letter = charEl.querySelector(".ub-token-glyph-letter"); const dot = charEl.querySelector(".ub-token-glyph-dot"); if (letter) { letter.style.transitionDelay = `${delay}ms`; letter.classList.toggle("ub-token-shown", !masked); } if (dot) { dot.style.transitionDelay = `${delay}ms`; dot.classList.toggle("ub-token-shown", masked); } }); syncOverlayScroll(); } sessionToken?.addEventListener("focus", () => { tokenMasked = false; setTokenMasked(false); }); sessionToken?.addEventListener("blur", () => { if (!sessionToken.value) { tokenMasked = true; return; } requestAnimationFrame(() => { tokenMasked = true; setTokenMasked(true); }); }); sessionToken?.addEventListener("input", () => { buildTokenChars(document.activeElement === sessionToken ? false : tokenMasked); }); sessionToken?.addEventListener("scroll", syncOverlayScroll); const guidelinesPanel = $("ub-guidelines-panel"); const guidelinesBackdrop = $("ub-guidelines-backdrop"); const CRT_CLOSE_ANIM_MS = 340; let guidelinesClosing = false; function openGuidelines() { if (!guidelinesPanel) return; guidelinesClosing = false; guidelinesPanel.classList.remove("ub-panel-closing"); void guidelinesPanel.offsetWidth; guidelinesPanel.classList.add("ub-panel-open"); if (guidelinesBackdrop) guidelinesBackdrop.classList.add("ub-backdrop-open"); } function closeGuidelines() { if (!guidelinesPanel || guidelinesClosing || !guidelinesPanel.classList.contains("ub-panel-open")) return; guidelinesClosing = true; if (guidelinesBackdrop) guidelinesBackdrop.classList.remove("ub-backdrop-open"); guidelinesPanel.classList.add("ub-panel-closing"); setTimeout(() => { guidelinesPanel.classList.remove("ub-panel-open", "ub-panel-closing"); guidelinesClosing = false; }, CRT_CLOSE_ANIM_MS); } $("ub-view-guidelines")?.addEventListener("click", openGuidelines); $("ub-guidelines-close")?.addEventListener("click", closeGuidelines); $("ub-guidelines-close-btn")?.addEventListener("click", closeGuidelines); guidelinesBackdrop?.addEventListener("click", closeGuidelines); function updatePopupLockState() { const locked = settings.store.badgeMode === "vencord"; showPopupRow?.classList.toggle("ub-disabled", locked); if (showPopupSwitch) showPopupSwitch.disabled = locked; showOwnerTagRow?.classList.toggle("ub-disabled", locked); if (showOwnerTagSwitch) showOwnerTagSwitch.disabled = locked; } wireDropdown("ub-badge-mode-dropdown", "ub-badge-mode", "ub-badge-mode-value", val => { settings.store.badgeMode = val; bridge.onBadgeModeChange(val); updatePopupLockState(); }); wireSwitch(showTooltipSwitch, val => { settings.store.showTooltip = val; }); wireSwitch(showPopupSwitch, val => { settings.store.showPopup = val; }); wireSwitch(showOwnerTagSwitch, val => { settings.store.showOwnerTag = val; }); wireSwitch(appendTagSwitch, val => { settings.store.appendTag = val; updatePreview(); }); wireSwitch(hideOwnBadgeSwitch, val => { settings.store.hideOwnBadge = val; }); wireDropdown("ub-selected-preset-dropdown", "ub-selected-preset", "ub-selected-preset-value", val => { settings.store.selectedPreset = val; }); function updateGlowFieldState() { glowColorField?.classList.toggle("ub-disabled", hoverEffectInput?.value !== "glow"); } function updateGradientFieldsState() { gradientFields?.classList.toggle("ub-disabled", bgModeInput?.value !== "edit"); } wireChoiceGroup("ub-icon-shape-group", "ub-icon-shape", val => { settings.store.badgeIconShape = val; updatePreview(); }); iconSize?.addEventListener("input", () => { if (iconSizeValue) iconSizeValue.textContent = `${iconSize.value}px`; settings.store.badgeIconSize = Number(iconSize.value); updatePreview(); }); iconSize?.addEventListener("change", () => { settings.store.badgeIconSize = Number(iconSize.value); updatePreview(); }); wireChoiceGroup("ub-hover-effect-group", "ub-hover-effect", val => { settings.store.badgeHoverEffect = val; updateGlowFieldState(); }); glowColor?.addEventListener("input", () => { if (glowColorHex) glowColorHex.textContent = glowColor.value.toUpperCase(); settings.store.badgeGlowColor = glowColor.value; }); glowColor?.addEventListener("change", () => { settings.store.badgeGlowColor = glowColor.value; }); wireChoiceGroup("ub-bg-mode-group", "ub-bg-mode", val => { settings.store.popupBackgroundMode = val; updateGradientFieldsState(); updatePreview(); }); gradientMain?.addEventListener("input", () => { if (gradientMainHex) gradientMainHex.textContent = gradientMain.value.toUpperCase(); settings.store.popupGradientMain = gradientMain.value; updatePreview(); }); gradientMain?.addEventListener("change", () => { settings.store.popupGradientMain = gradientMain.value; updatePreview(); }); gradientSecondary?.addEventListener("input", () => { if (gradientSecondaryHex) gradientSecondaryHex.textContent = gradientSecondary.value.toUpperCase(); settings.store.popupGradientSecondary = gradientSecondary.value; updatePreview(); }); gradientSecondary?.addEventListener("change", () => { settings.store.popupGradientSecondary = gradientSecondary.value; updatePreview(); }); nameColor?.addEventListener("input", () => { if (nameColorHex) nameColorHex.textContent = nameColor.value.toUpperCase(); settings.store.badgeNameColor = nameColor.value; updatePreview(); }); nameColor?.addEventListener("change", () => { settings.store.badgeNameColor = nameColor.value; updatePreview(); }); wireChoiceGroup("ub-popup-anim-group", "ub-popup-anim", val => { settings.store.popupAnimationStyle = val; }); const previewEmpties = Array.from(root.querySelectorAll(".ub-preview-empty")); const previewContents = Array.from(root.querySelectorAll(".ub-preview-content")); const previewRowIcons = Array.from(root.querySelectorAll(".ub-preview-row-icon")); const popupCards = Array.from(root.querySelectorAll(".ub-popup-card")); const popupImgs = Array.from(root.querySelectorAll(".ub-popup-img")); const popupNames = Array.from(root.querySelectorAll(".ub-popup-name")); const popupBys = Array.from(root.querySelectorAll(".ub-popup-by")); const previewWarnings = Array.from(root.querySelectorAll(".ub-preview-warning")); const radiusFor = shape => shape === "circle" ? "50%" : shape === "rounded" ? "6px" : "0"; let previewToken = 0; async function updatePreview() { const token = ++previewToken; const url = badgeImageUrl?.value.trim() ?? ""; const name = badgeName?.value.trim() ?? ""; if (!url || !name) { previewEmpties.forEach(el => el.style.display = ""); previewContents.forEach(el => el.style.display = "none"); return; } previewEmpties.forEach(el => el.style.display = "none"); previewContents.forEach(el => el.style.display = ""); const bridge = getDashboardBridge(); const data = await (bridge?.getPreviewData()); if (token !== previewToken) return; const radius = radiusFor(data?.iconShape ?? "circle"); previewRowIcons.forEach(el => { el.src = data?.imageUrl ?? url; el.style.width = `${data?.iconSize ?? 22}px`; el.style.height = `${data?.iconSize ?? 22}px`; el.style.borderRadius = radius; }); popupCards.forEach(el => { el.style.background = data?.background ?? "#1d1d1d"; }); popupImgs.forEach(el => { el.src = data?.imageUrl ?? url; el.style.borderRadius = radius; }); popupNames.forEach(el => { el.textContent = data?.displayName ?? name; el.style.color = data?.nameColor ?? "#ffffff"; }); popupBys.forEach(el => { el.textContent = data?.ownerTag ?? ""; el.style.display = data?.ownerTag ? "" : "none"; }); previewWarnings.forEach(el => { el.style.display = data?.sampleFailed ? "" : "none"; }); } const myBadgesListEl = $("ub-my-badges-list"); function renderMyBadgesList() { if (!myBadgesListEl) return; let badges = []; try { badges = JSON.parse(settings.store.myBadgesJson || "[]"); } catch { badges = []; } const activeId = settings.store.myActiveBadgeId ?? ""; if (!badges.length) { myBadgesListEl.innerHTML = ""; return; } myBadgesListEl.innerHTML = badges.map(b => { const isActive = b.id === activeId; return `\n
\n ${b.description || \n \n ${b.description || "Unnamed badge"}${isActive ? `(active)` : ""}\n \n
\n ${!isActive ? `` : ""}\n \n
\n
\n `; }).join(""); myBadgesListEl.querySelectorAll(".ub-badge-use-btn").forEach(btn => { btn.addEventListener("click", async () => { const id = btn.dataset.useId; if (!id) return; btn.disabled = true; btn.textContent = "..."; try { await bridge.switchToBadge(id); } finally { syncFromStore(); } }); }); myBadgesListEl.querySelectorAll(".ub-badge-delete-btn").forEach(btn => { btn.addEventListener("click", async () => { const id = btn.dataset.deleteId; if (!id) return; btn.disabled = true; btn.textContent = "..."; try { await bridge.deleteBadgeSlot(id); } finally { syncFromStore(); } }); }); } function syncFromStore() { if (apiBaseUrl) apiBaseUrl.value = settings.store.apiBaseUrl ?? ""; if (badgeImageUrl) badgeImageUrl.value = settings.store.myBadgeImageUrl ?? ""; if (badgeName) badgeName.value = settings.store.myBadgeName ?? ""; if (sessionToken) sessionToken.value = settings.store.sessionToken ?? ""; if (selfUserId) selfUserId.value = settings.store.selfUserId ?? ""; tokenMasked = document.activeElement === sessionToken ? false : !!settings.store.sessionToken; buildTokenChars(tokenMasked); if (revokeTokenBtn) revokeTokenBtn.disabled = !settings.store.sessionToken; renderMyBadgesList(); const modeVal = settings.store.badgeMode ?? "original"; if (badgeModeInput) badgeModeInput.value = modeVal; const modeOpt = root.querySelector(`#ub-badge-mode-menu .ub-dropdown-option[data-value="${modeVal}"]`); if (modeOpt) { root.querySelector("#ub-badge-mode-value").textContent = modeOpt.textContent ?? ""; root.querySelectorAll("#ub-badge-mode-menu .ub-dropdown-option").forEach(o => o.classList.remove("selected")); modeOpt.classList.add("selected"); } const presetVal = String(settings.store.selectedPreset ?? "0"); if (selectedPresetInput) selectedPresetInput.value = presetVal; const presetOpt = root.querySelector(`#ub-selected-preset-menu .ub-dropdown-option[data-value="${presetVal}"]`); if (presetOpt) { root.querySelector("#ub-selected-preset-value").textContent = presetOpt.textContent ?? ""; root.querySelectorAll("#ub-selected-preset-menu .ub-dropdown-option").forEach(o => o.classList.remove("selected")); presetOpt.classList.add("selected"); } setChoiceGroupValue("ub-icon-shape-group", "ub-icon-shape", settings.store.badgeIconShape ?? "circle"); const sizeVal = settings.store.badgeIconSize ?? 22; if (iconSize) iconSize.value = String(sizeVal); if (iconSizeValue) iconSizeValue.textContent = `${sizeVal}px`; setChoiceGroupValue("ub-hover-effect-group", "ub-hover-effect", settings.store.badgeHoverEffect ?? "none"); const glowVal = settings.store.badgeGlowColor ?? "#ffffff"; if (glowColor) glowColor.value = glowVal; if (glowColorHex) glowColorHex.textContent = glowVal.toUpperCase(); updateGlowFieldState(); setChoiceGroupValue("ub-bg-mode-group", "ub-bg-mode", settings.store.popupBackgroundMode ?? "base"); const gradMainVal = settings.store.popupGradientMain ?? "#1d1d1d"; if (gradientMain) gradientMain.value = gradMainVal; if (gradientMainHex) gradientMainHex.textContent = gradMainVal.toUpperCase(); const gradSecVal = settings.store.popupGradientSecondary ?? "#2a2a38"; if (gradientSecondary) gradientSecondary.value = gradSecVal; if (gradientSecondaryHex) gradientSecondaryHex.textContent = gradSecVal.toUpperCase(); updateGradientFieldsState(); const nameColorVal = settings.store.badgeNameColor ?? "#ffffff"; if (nameColor) nameColor.value = nameColorVal; if (nameColorHex) nameColorHex.textContent = nameColorVal.toUpperCase(); setChoiceGroupValue("ub-popup-anim-group", "ub-popup-anim", settings.store.popupAnimationStyle ?? "fade"); setSwitchValue(showTooltipSwitch, settings.store.showTooltip ?? true); setSwitchValue(showPopupSwitch, settings.store.showPopup ?? true); setSwitchValue(showOwnerTagSwitch, settings.store.showOwnerTag ?? true); setSwitchValue(appendTagSwitch, settings.store.appendTag ?? false); setSwitchValue(hideOwnBadgeSwitch, settings.store.hideOwnBadge ?? false); updatePopupLockState(); updatePreview(); } syncFromStore(); apiBaseUrl?.addEventListener("change", () => { settings.store.apiBaseUrl = apiBaseUrl.value; }); badgeImageUrl?.addEventListener("input", () => { settings.store.myBadgeImageUrl = badgeImageUrl.value; updatePreview(); }); badgeImageUrl?.addEventListener("change", () => { settings.store.myBadgeImageUrl = badgeImageUrl.value; updatePreview(); }); badgeName?.addEventListener("input", () => { settings.store.myBadgeName = badgeName.value; updatePreview(); }); badgeName?.addEventListener("change", () => { settings.store.myBadgeName = badgeName.value; updatePreview(); }); const applyBadgeBtn = $("ub-apply-badge"); applyBadgeBtn?.addEventListener("click", async () => { if (applyBadgeBtn.disabled) return; applyBadgeBtn.disabled = true; const originalLabel = applyBadgeBtn.textContent; applyBadgeBtn.textContent = "Applying..."; try { await bridge.applyBadgeNow(); } finally { applyBadgeBtn.disabled = false; applyBadgeBtn.textContent = originalLabel; } }); $("ub-share-badge")?.addEventListener("click", () => bridge.shareMyBadge()); $("ub-revert-badge")?.addEventListener("click", () => { bridge.revertBadge(); syncFromStore(); }); $("ub-refresh-cache")?.addEventListener("click", () => bridge.refreshBadgeCache()); $("ub-import-badge")?.addEventListener("click", () => { settings.store.importBadgeCode = importBadgeCode?.value ?? ""; bridge.importBadgeFromCode(); if (importBadgeCode) importBadgeCode.value = ""; syncFromStore(); }); $("ub-apply-preset")?.addEventListener("click", () => { bridge.applySelectedPreset(); syncFromStore(); }); $("ub-new-badge-slot")?.addEventListener("click", () => { bridge.createNewBadgeSlot(); syncFromStore(); }); $("ub-import-pack")?.addEventListener("click", () => { settings.store.importPackUrl = importPackUrl?.value ?? ""; bridge.importPackFromUrl(); syncFromStore(); }); $("ub-make-pack")?.addEventListener("click", () => { bridge.makePack(); if (!settings.store.packGuidelinesShown) { settings.store.packGuidelinesShown = true; openGuidelines(); } }); $("ub-browse-packs")?.addEventListener("click", () => bridge.browsePacks()); $("ub-verify-account")?.addEventListener("click", () => bridge.verifyAccount()); sessionToken?.addEventListener("change", () => { settings.store.sessionToken = sessionToken.value; if (revokeTokenBtn) revokeTokenBtn.disabled = !settings.store.sessionToken; fetchWriteBudget(); }); selfUserId?.addEventListener("change", () => { const val = selfUserId.value.trim(); if (val && !/^\d{15,25}$/.test(val)) { toast("That doesn't look like a Discord User ID (should be a 15-25 digit number)", { type: "error" }); return; } settings.store.selfUserId = val; if (val) toast("Manual User ID saved - publishing will use this instead of auto-detection", { type: "success" }); }); revokeTokenBtn?.addEventListener("click", async () => { if (!settings.store.sessionToken || revokeTokenBtn.disabled) return; revokeTokenBtn.disabled = true; const originalLabel = revokeTokenBtn.textContent; revokeTokenBtn.textContent = "Revoking..."; try { await bridge.revokeSessionToken(); } finally { syncFromStore(); if (revokeTokenBtn.textContent === "Revoking...") revokeTokenBtn.textContent = originalLabel; } }); } const DIRECTORY_GRID_SELECTOR = '[data-list-id="badge-directory-owned"]'; const DIRECTORY_ATTR = "data-cb-directory"; const DIRECTORY_WARM_MAX_ATTEMPTS = 30; const DIRECTORY_RARITY_LABEL = "Mythic"; let _directorySelection = null; function ownBadgeInfo() { const me = getCurrentUser(); if (!me || getSetting("hideOwnBadge")) return null; const entry = badgeCache.get(me.id); const data = entry && entry.data; if (!data || !data.imageUrl) return null; const style = resolveBadgeStyle(data.style, data.firstRequestAt); const date = style.firstUsedDate ? new Date(style.firstUsedDate) : null; return { imageUrl: data.imageUrl, name: formatBadgeName(data.description || "Custom Badge", style.appendTag), date: date && !isNaN(date.getTime()) ? date : null }; } function buildDirectoryPanel(info) { const panel = document.createElement("div"); panel.id = "cb-badge-details"; panel.setAttribute("role", "tabpanel"); panel.style.cssText = "display:flex;align-items:center;gap:16px;padding:16px 4px;color:var(--text-default, #dbdee1);"; const img = document.createElement("img"); img.src = info.imageUrl; img.alt = ""; img.referrerPolicy = "no-referrer"; img.draggable = false; img.style.cssText = "width:80px;height:80px;object-fit:contain;flex-shrink:0;"; const col = document.createElement("div"); col.style.cssText = "display:flex;flex-direction:column;gap:4px;min-width:0;"; const muted = "font-size:14px;line-height:18px;color:var(--text-muted, #949ba4);"; const rows = [ [ info.name, "font-size:20px;font-weight:700;line-height:24px;color:var(--header-primary, #f2f3f5);" ], [ DIRECTORY_RARITY_LABEL, "font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:.02em;color:var(--text-muted, #949ba4);" ], [ "CustomBadges User", "font-size:14px;line-height:18px;" ], [ "Custom Badges", muted ] ]; if (info.date) rows.push([ "Unlocked " + info.date.toLocaleDateString(), muted ]); rows.forEach(([text, css]) => { const row = document.createElement("div"); row.textContent = text; row.style.cssText = css; col.appendChild(row); }); panel.append(img, col); return panel; } function buildNativePanel(source, info) { try { const clone = source.cloneNode(true); clone.id = "cb-badge-details"; clone.style.display = ""; const img = clone.querySelector('img[class*="badgeGraphic"]'); const title = clone.querySelector("h2"); const line = clone.querySelector('[class*="badgeAccessoryLine"]'); const rar = clone.querySelector('[class*="rarity-"]'); const raritySeg = rar && rar.closest('[class*="badgeAccessorySegment"]'); const textSegs = Array.from(clone.querySelectorAll('[class*="badgeAccessorySegment"]')).filter(s => s !== raritySeg); if (!img || !title || !line || !textSegs.length) return null; const setText = (seg, text) => { const el = seg.querySelector('[class*="badgeAccessoryText"]'); if (el) el.textContent = text; }; const dot = clone.querySelector('svg[class*="badgeAccessoryDot"]'); img.src = info.imageUrl; img.referrerPolicy = "no-referrer"; img.style.objectFit = "contain"; title.textContent = info.name; setText(textSegs[0], "Custom Badges"); textSegs.slice(1).forEach(s => s.remove()); if (info.date) { const seg = textSegs[0].cloneNode(true); if (dot) seg.prepend(dot.cloneNode(true)); setText(seg, "Unlocked " + info.date.toLocaleDateString(undefined, { year: "2-digit", month: "numeric", day: "numeric" })); textSegs[0].after(seg); } if (rar) { rar.className = rar.className.replace(/rarity-[a-z]+(?=_)/, "rarity-mythic"); const label = rar.querySelector('[class*="label"]'); if (label) label.textContent = DIRECTORY_RARITY_LABEL; } const bd = clone.querySelector('[class*="badgeDetails__"]'); if (bd) Array.from(bd.children).slice(1).forEach(c => c.remove()); const card = clone.querySelector('[class*="descriptionCard__"]'); if (card) Array.from(card.children).slice(1).forEach(c => c.remove()); clone.querySelectorAll('[class*="legacyLabel"]').forEach(el => el.remove()); const descs = Array.from(clone.querySelectorAll('[class*="descriptionText__"]')); if (descs[0]) descs[0].textContent = "CustomBadges User"; descs.slice(1).forEach(el => el.remove()); return clone; } catch (e) { return null; } } function deselectDirectoryBadge() { const s = _directorySelection; _directorySelection = null; if (!s) return; if (s.panel) s.panel.remove(); if (s.details) s.details.style.display = s.detailsDisplay; if (s.slot && s.slot.isConnected) { s.slot.className = s.slotClass; s.slot.setAttribute("aria-selected", "false"); } if (s.native && s.native.isConnected) { s.native.className = s.nativeClass; s.native.setAttribute("aria-selected", "true"); } } function selectDirectoryBadge(grid, slot, info) { if (_directorySelection) return; const native = grid.querySelector(`[role="tab"][aria-selected="true"]:not([${DIRECTORY_ATTR}])`); const selectedToken = native && Array.from(native.classList).find(c => /Selected/.test(c)); const root = document.getElementById("badge-details"); const scroller = root && (root.matches('[class*="detailScroller"]') ? root : root.querySelector('[class*="detailScroller"]')); const details = scroller || root; const panel = scroller && buildNativePanel(scroller, info) || buildDirectoryPanel(info); _directorySelection = { slot: slot, slotClass: slot.className, native: native, nativeClass: native ? native.className : "", details: details, detailsDisplay: details ? details.style.display : "", panel: panel }; if (native && selectedToken) { native.classList.remove(selectedToken); native.setAttribute("aria-selected", "false"); } if (selectedToken) slot.classList.add(selectedToken); slot.setAttribute("aria-selected", "true"); if (details) { details.style.display = "none"; details.insertAdjacentElement("afterend", panel); } else { grid.insertAdjacentElement("afterend", panel); } } function injectDirectoryBadge() { if (_directorySelection && !_directorySelection.slot.isConnected) _directorySelection = null; const grid = document.querySelector(DIRECTORY_GRID_SELECTOR); if (!grid) return; const info = ownBadgeInfo(); let slot = grid.querySelector(`[${DIRECTORY_ATTR}]`); const key = info ? `${info.imageUrl}|${info.name}|${info.date ? info.date.getTime() : ""}` : ""; if (slot && info && slot.dataset.cbKey === key) return; if (slot) { deselectDirectoryBadge(); slot.remove(); } if (!info) return; const template = grid.querySelector(`[role="tab"]:not([${DIRECTORY_ATTR}])`); if (!template) return; slot = template.cloneNode(true); Array.from(slot.classList).filter(c => /Selected/.test(c)).forEach(c => slot.classList.remove(c)); slot.setAttribute(DIRECTORY_ATTR, "1"); slot.dataset.cbKey = key; slot.id = "badge-tab-cb"; slot.tabIndex = -1; slot.setAttribute("aria-label", info.name); slot.setAttribute("aria-selected", "false"); slot.setAttribute("data-list-item-id", `badge-directory-owned___item-${grid.querySelectorAll('[role="tab"]').length}`); const icon = slot.querySelector("img"); if (icon) { icon.src = info.imageUrl; icon.referrerPolicy = "no-referrer"; icon.style.objectFit = "contain"; } slot.addEventListener("click", e => { e.preventDefault(); e.stopPropagation(); selectDirectoryBadge(grid, slot, info); }); grid.appendChild(slot); } document.addEventListener("click", e => { if (!_directorySelection || !(e.target instanceof Element)) return; const tab = e.target.closest('[role="tab"]'); if (tab && !tab.hasAttribute(DIRECTORY_ATTR) && tab.closest(DIRECTORY_GRID_SELECTOR)) deselectDirectoryBadge(); }, true); function warmOwnBadge(attempt) { try { const me = getCurrentUser(); if (me) { fetchBadge(me.id).then(injectDirectoryBadge, () => {}); return; } } catch (e) {} if (attempt < DIRECTORY_WARM_MAX_ATTEMPTS) setTimeout(() => warmOwnBadge(attempt + 1), 1e3); } function init() { log("v11 tooltip + card initializing..."); initDashboard(); scan(); new MutationObserver(scan).observe(document.body, { childList: true, subtree: true }); setInterval(scan, 500); warmOwnBadge(0); } if (document.body) init(); else document.addEventListener("DOMContentLoaded", init); window.refreshCustomBadges = () => { document.querySelectorAll(`[${MARKER}]`).forEach(el => el.removeAttribute(MARKER)); badgeCache.clear(); scan(); }; })();