This commit is contained in:
Clove 2026-06-20 18:34:31 +01:00
parent 8549f46d9b
commit 71dbd01d52
13 changed files with 151 additions and 37 deletions

BIN
assets/fonts/8Bit.woff2 Normal file

Binary file not shown.

Binary file not shown.

BIN
assets/fonts/Medieval.woff2 Normal file

Binary file not shown.

BIN
assets/fonts/Modern.woff2 Normal file

Binary file not shown.

BIN
assets/fonts/Sakura.woff2 Normal file

Binary file not shown.

BIN
assets/fonts/Tempo.woff2 Normal file

Binary file not shown.

BIN
assets/fonts/Vampyre.woff2 Normal file

Binary file not shown.

BIN
assets/fonts/gg sans.woff2 Normal file

Binary file not shown.

View File

@ -29,4 +29,50 @@
url('https://fonts.doughmination.co.uk/ComicCode-Bold_2022-05-24-152309_zqkm.woff') format('woff'); url('https://fonts.doughmination.co.uk/ComicCode-Bold_2022-05-24-152309_zqkm.woff') format('woff');
font-weight: 700; font-weight: 700;
font-style: normal; font-style: normal;
}
/* ---- Discord display-name style fonts (look-alikes, in /assets/fonts) -------
Applied to .pc-name from display_name_styles.font_id (see js/discord.js).
Discord font_id -> file (confirmed): 3 Cherry Bomb=Sakura, 4 Chicle=Jellybean,
6 MuseoModerno=Modern, 7 Néo-Castel=Medieval, 8 Pixelify=8Bit, 10 Sinistre=Vampyre,
11 Default=gg sans, 12 Zilla Slab=Tempo. (1/2/5/9 have no file -> site font.) */
@font-face {
font-family: 'DDN 8Bit';
src: url('/assets/fonts/8Bit.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN Jellybean';
src: url('/assets/fonts/Jellybean.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN Medieval';
src: url('/assets/fonts/Medieval.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN Modern';
src: url('/assets/fonts/Modern.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN Sakura';
src: url('/assets/fonts/Sakura.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN Tempo';
src: url('/assets/fonts/Tempo.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN Vampyre';
src: url('/assets/fonts/Vampyre.woff2') format('woff2');
font-display: swap;
}
@font-face {
font-family: 'DDN gg sans';
src: url('/assets/fonts/gg%20sans.woff2') format('woff2');
font-display: swap;
} }

View File

@ -3386,7 +3386,22 @@ a.pc-row:hover,
} }
a.pc-wl-item:hover { background: var(--mantle); } a.pc-wl-item:hover { background: var(--mantle); }
.pc-wl-ic { width: 22px; height: 22px; border-radius: 5px; object-fit: cover; } .pc-wl-ic { width: 22px; height: 22px; border-radius: 5px; object-fit: cover; }
.pc-wl-text { display: flex; flex-direction: column; line-height: 1.2; min-width: 0; }
.pc-wl-name { font-size: 0.8rem; } .pc-wl-name { font-size: 0.8rem; }
.pc-wl-type {
font-size: 0.6rem;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--subtext-0);
}
.pc-wl-price {
margin-left: auto;
padding-left: 0.5rem;
font-size: 0.72rem;
color: var(--subtext-1);
white-space: nowrap;
}
.pc-wl-item.is-owned { opacity: 0.5; }
.pc-wl-empty { font-size: 0.78rem; color: var(--subtext-0); margin: 0; } .pc-wl-empty { font-size: 0.78rem; color: var(--subtext-0); margin: 0; }
/* Discord profile gradient (Catppuccin is the fallback) */ /* Discord profile gradient (Catppuccin is the fallback) */

View File

@ -86,23 +86,60 @@
const pronounsEl = card.querySelector(".pc-pronouns"); const pronounsEl = card.querySelector(".pc-pronouns");
// ---- wishlist (revealed by the star) ------------------------------------ // ---- wishlist (revealed by the star) ------------------------------------
// Items come straight from the Doughmination Restful API (j.data.wishlist):
// each is a resolved Shop item { sku_id, type, name, static_image_url,
// animated_image_url, video_url, label, is_owned, price, visibility }.
let wishlistItems = null; let wishlistItems = null;
const WL_TYPE_LABEL = {
avatar_decoration: "Decoration",
profile_effect: "Effect",
nameplate: "Nameplate",
bundle: "Bundle",
variants_group: "Variants",
external_sku: "Item"
};
const CURRENCY_SYMBOL = { gbp: "£", usd: "$", eur: "€", aud: "A$", cad: "C$" };
function fmtPrice(p) {
if (!p || typeof p.amount !== "number") return null;
const exp = typeof p.exponent === "number" ? p.exponent : 2;
const v = (p.amount / Math.pow(10, exp)).toFixed(exp);
const sym = CURRENCY_SYMBOL[(p.currency || "").toLowerCase()];
return sym ? sym + v : v + " " + String(p.currency || "").toUpperCase();
}
// Pick a thumbnail and decide whether the wsrv webp proxy is safe: avatar
// decorations and profile effects are animated APNGs the proxy mangles (the
// same reason the avatar decoration loads raw), so those go straight to the
// CDN; nameplates and the rest are static and proxy fine.
function wlImg(w) {
const url = w.static_image_url || w.animated_image_url;
if (!url) return null;
if (w.type === "avatar_decoration" || w.type === "profile_effect" || /avatar-decoration-presets/.test(url)) {
return url;
}
return proxyImg(url, { w: 64 }) || url;
}
function renderWishlist() { function renderWishlist() {
if (!wishlistEl) return; if (!wishlistEl) return;
if (wishlistItems && wishlistItems.length) { const items = Array.isArray(wishlistItems) ? wishlistItems : [];
wishlistEl.innerHTML = '<div class="pc-wishlist-title">Wishlist</div>' + let body;
wishlistItems.map(function (w) { if (items.length) {
const inner = body = items.map(function (w) {
(w.icon ? '<img class="pc-wl-ic" src="' + esc(w.icon) + '" alt="">' : "") + const ic = wlImg(w);
'<span class="pc-wl-name">' + esc(w.name || "") + "</span>"; const typeLabel = WL_TYPE_LABEL[w.type] || "";
return w.url const price = fmtPrice(w.price);
? '<a class="pc-wl-item" href="' + esc(w.url) + '" target="_blank" rel="noopener">' + inner + "</a>" return '<span class="pc-wl-item' + (w.is_owned ? " is-owned" : "") + '">' +
: '<span class="pc-wl-item">' + inner + "</span>"; (ic ? '<img class="pc-wl-ic" src="' + esc(ic) + '" alt="" loading="lazy" referrerpolicy="no-referrer" onerror="this.remove()">' : "") +
}).join(""); '<span class="pc-wl-text">' +
'<span class="pc-wl-name">' + esc(w.name || "Collectible") + "</span>" +
(typeLabel ? '<span class="pc-wl-type">' + esc(typeLabel) + "</span>" : "") +
"</span>" +
(price ? '<span class="pc-wl-price">' + esc(price) + "</span>" : "") +
"</span>";
}).join("");
} else { } else {
wishlistEl.innerHTML = '<div class="pc-wishlist-title">Wishlist</div>' + body = '<p class="pc-wl-empty">nothing on the wishlist yet ✨</p>';
'<p class="pc-wl-empty">coming soon ✨</p>';
} }
wishlistEl.innerHTML = '<div class="pc-wishlist-title">Wishlist</div>' + body;
} }
if (starBtn) { if (starBtn) {
starBtn.addEventListener("click", function (e) { starBtn.addEventListener("click", function (e) {
@ -476,6 +513,21 @@
return row; return row;
} }
// Discord display-name fonts (display_name_styles.font_id) -> our @font-face
// families in css/fonts.css. Only ids we have a look-alike for are mapped;
// any other id falls back to the card's normal font. (Comments = Discord's
// underlying font; "verify" = best-guess pairing with your file names.)
const NAME_FONTS = {
3: "'DDN Sakura', cursive", // 3 CHERRY_BOMB
4: "'DDN Jellybean', cursive", // 4 CHICLE
6: "'DDN Modern', sans-serif", // 6 MUSEO_MODERNO
7: "'DDN Medieval', serif", // 7 NEO_CASTEL
8: "'DDN 8Bit', monospace", // 8 PIXELIFY
10: "'DDN Vampyre', serif", // 10 SINISTRE
11: "'DDN gg sans', sans-serif", // 11 DEFAULT (Discord's normal font)
12: "'DDN Tempo', serif", // 12 ZILLA_SLAB
};
// ---- render ------------------------------------------------------------- // ---- render -------------------------------------------------------------
function render(d) { function render(d) {
if (!d) return; if (!d) return;
@ -508,6 +560,9 @@
nameEl.style.backgroundImage = ""; nameEl.style.backgroundImage = "";
nameEl.classList.remove("is-gradient"); nameEl.classList.remove("is-gradient");
} }
// Custom display-name font from Discord's font_id (falls back to the
// card's normal font when there's no style or no look-alike for that id).
nameEl.style.fontFamily = (styles && NAME_FONTS[styles.font_id]) || "";
const pg = u.primary_guild; const pg = u.primary_guild;
if (pg && pg.tag && pg.identity_enabled) { if (pg && pg.tag && pg.identity_enabled) {
@ -613,7 +668,10 @@
primary_guild: (clan && clan.tag) primary_guild: (clan && clan.tag)
? { tag: clan.tag, identity_enabled: true, badge: clan.badge, identity_guild_id: clan.guild_id } ? { tag: clan.tag, identity_enabled: true, badge: clan.badge, identity_guild_id: clan.guild_id }
: null, : null,
public_flags: 0 // carry the Nitro name styling through so render() can apply the
// gradient + custom font (font_id) — without this it never reaches it
display_name_styles: u.display_name_styles || null,
public_flags: u.public_flags || 0
}, },
discord_status: p.status || (p.online ? "online" : "offline"), discord_status: p.status || (p.online ? "online" : "offline"),
activities: p.activities || [], activities: p.activities || [],
@ -649,6 +707,10 @@
if (u.pronouns) { pronounsEl.textContent = u.pronouns; pronounsEl.hidden = false; } if (u.pronouns) { pronounsEl.textContent = u.pronouns; pronounsEl.hidden = false; }
else pronounsEl.hidden = true; else pronounsEl.hidden = true;
} }
// wishlist: resolved Shop collectibles (null when the API couldn't load it).
// Keep the panel live if it's already open when fresh data arrives.
wishlistItems = Array.isArray(j.data.wishlist) ? j.data.wishlist : null;
if (card.classList.contains("show-wishlist")) renderWishlist();
} }
function loadSelfHosted() { function loadSelfHosted() {

View File

@ -86,29 +86,20 @@
} }
// Nitro "display name styles" -> gradient on the name text (same effect the // Nitro "display name styles" -> gradient on the name text (same effect the
// /discord card uses). Lanyard + dstn.to expose this; our API does not yet. // /discord card uses). Lanyard + dstn.to expose this; our API does not yet.
// Discord's 8 display-name fonts -> closest free Google Fonts (approximations; // Discord display-name fonts (display_name_styles.font_id) -> the local
// Discord's own faces are proprietary). font_id 1 = gg sans = our default. // look-alike @font-face families in css/fonts.css (loaded site-wide via
// main.css @import). Mapping confirmed against Discord's font_id enum;
// mirrors NAME_FONTS in js/discord.js. (1/2/5/9 have no file -> default.)
var FONT_BY_ID = { var FONT_BY_ID = {
2: '"Poppins", sans-serif', // Tempo 3: "'DDN Sakura', cursive", // CHERRY_BOMB
3: '"Klee One", cursive', // Sakura 4: "'DDN Jellybean', cursive", // CHICLE
4: '"Baloo 2", cursive', // Jellybean 6: "'DDN Modern', sans-serif", // MUSEO_MODERNO
5: '"Montserrat", sans-serif', // Modern 7: "'DDN Medieval', serif", // NEO_CASTEL
6: '"MedievalSharp", cursive', // Medieval 8: "'DDN 8Bit', monospace", // PIXELIFY
7: '"Press Start 2P", monospace', // 8Bit 10: "'DDN Vampyre', serif", // SINISTRE
8: '"Pirata One", system-ui' // Vampyre 11: "'DDN gg sans', sans-serif", // DEFAULT
12: "'DDN Tempo', serif" // ZILLA_SLAB
}; };
var GFONTS_HREF = "https://fonts.googleapis.com/css2?" +
"family=Baloo+2:wght@600&family=Klee+One:wght@600&family=MedievalSharp&" +
"family=Montserrat:wght@700&family=Pirata+One&family=Poppins:wght@600&" +
"family=Press+Start+2P&display=swap";
var _fontsInjected = false;
function ensureFonts() {
if (_fontsInjected) return;
_fontsInjected = true;
var l = document.createElement("link");
l.rel = "stylesheet"; l.href = GFONTS_HREF;
document.head.appendChild(l);
}
function applyNameStyle(refs, styles) { function applyNameStyle(refs, styles) {
if (!refs.name || !styles) return; if (!refs.name || !styles) return;
// gradient / colour // gradient / colour
@ -118,9 +109,9 @@
(cols.length === 1 ? cols[0] + "," + cols[0] : cols.join(", ")) + ")"; (cols.length === 1 ? cols[0] + "," + cols[0] : cols.join(", ")) + ")";
refs.name.classList.add("is-gradient"); refs.name.classList.add("is-gradient");
} }
// font face (lazy-load the Google Fonts sheet only when one is used) // custom display-name font (local @font-face; no network load needed)
var fam = styles.font_id && FONT_BY_ID[styles.font_id]; var fam = styles.font_id && FONT_BY_ID[styles.font_id];
if (fam) { ensureFonts(); refs.name.style.fontFamily = fam; } if (fam) { refs.name.style.fontFamily = fam; }
} }
// Re-serve Discord CDN images cookieless via wsrv.nl (same trick as the // Re-serve Discord CDN images cookieless via wsrv.nl (same trick as the
// presence card) so no third-party cookies are set. // presence card) so no third-party cookies are set.

View File

@ -10,7 +10,7 @@ I made this cause I kept losing track of everything, and yeah...
## License ## License
This repository is licenced under MIT, meaning if you wish to copy it, that's fine, if it breaks, don't blame me. See the <a href="./LICENSE">LICENSE</a> for more details! This repository is licenced under ESAL-1.5, please review the terms before copying this code.
## Codeowners and Contributors ## Codeowners and Contributors
All code is owned and created by myself, Clove Twilight. All code is owned and created by myself, Clove Twilight.