Fixes
This commit is contained in:
parent
ec7f9cf1a7
commit
44f47fe56b
66
css/main.css
66
css/main.css
|
|
@ -1996,16 +1996,39 @@ body:has(.friends-wrap) .hub {
|
||||||
box-shadow: 0 8px 22px -12px rgba(var(--accent-rgb), 0.5);
|
box-shadow: 0 8px 22px -12px rgba(var(--accent-rgb), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Nitro profile gradient — recolours the card body (mirrors the /discord card) */
|
||||||
|
.friend-card.has-profile-grad {
|
||||||
|
background: linear-gradient(180deg, rgb(var(--fc-grad-1-rgb)) 0%, rgb(var(--fc-grad-2-rgb)) 100%);
|
||||||
|
border-color: rgba(var(--fc-grad-1-rgb), 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
/* Hovered card lifts above its neighbours and lets its text un-truncate so
|
/* Hovered card lifts above its neighbours and lets its text un-truncate so
|
||||||
long names / usernames / custom statuses expand in full instead of being
|
long names / usernames / custom statuses expand in full instead of being
|
||||||
chopped off with an ellipsis. */
|
chopped off with an ellipsis. The body becomes an absolutely-positioned
|
||||||
.friend-card:hover { z-index: 5; }
|
overlay, so the card grows DOWNWARD without resizing its grid slot —
|
||||||
.friend-card:hover .fc-name,
|
neighbouring cards stay exactly where they are. */
|
||||||
.friend-card:hover .fc-user,
|
/* Expansion is gated on .is-hovering (added by JS) rather than :hover so the
|
||||||
.friend-card:hover .fc-custom-text {
|
card's flow height can be pinned BEFORE .fc-main is taken out of flow —
|
||||||
|
otherwise the card collapses, the page reflows, and hover ping-pongs
|
||||||
|
(the "vibrating page" bug). */
|
||||||
|
.friend-card.is-hovering { overflow: visible; z-index: 5; }
|
||||||
|
.friend-card.is-hovering .fc-main {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: 54px; /* sit directly under the banner strip */
|
||||||
|
background: inherit; /* carry the card surface / Nitro gradient */
|
||||||
|
border-bottom-left-radius: 14px;
|
||||||
|
border-bottom-right-radius: 14px;
|
||||||
|
box-shadow: 0 10px 24px -14px rgba(17, 17, 27, 0.8);
|
||||||
|
}
|
||||||
|
.friend-card.is-hovering .fc-name,
|
||||||
|
.friend-card.is-hovering .fc-user,
|
||||||
|
.friend-card.is-hovering .fc-custom-text {
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-overflow: clip;
|
text-overflow: clip;
|
||||||
|
overflow-wrap: anywhere; /* break long unbroken usernames / handles */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* banner strip — Nitro banner image / accent colour / default wash */
|
/* banner strip — Nitro banner image / accent colour / default wash */
|
||||||
|
|
@ -2015,6 +2038,10 @@ body:has(.friends-wrap) .hub {
|
||||||
linear-gradient(135deg, rgba(var(--accent-rgb), 0.45), rgba(var(--accent-rgb), 0.12));
|
linear-gradient(135deg, rgba(var(--accent-rgb), 0.45), rgba(var(--accent-rgb), 0.12));
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
|
/* keep the banner corners rounded even on hover, when the card switches
|
||||||
|
to overflow:visible and no longer clips them to the card radius */
|
||||||
|
border-top-left-radius: 14px;
|
||||||
|
border-top-right-radius: 14px;
|
||||||
}
|
}
|
||||||
.friend-card.has-banner .fc-banner {
|
.friend-card.has-banner .fc-banner {
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
|
|
@ -2121,6 +2148,16 @@ body:has(.friends-wrap) .hub {
|
||||||
}
|
}
|
||||||
a.fc-name:hover { color: rgb(var(--accent-rgb)); }
|
a.fc-name:hover { color: rgb(var(--accent-rgb)); }
|
||||||
|
|
||||||
|
/* Nitro display-name gradient (clipped to the text). Keep the tier heart in
|
||||||
|
::before painted normally so it stays its solid colour. */
|
||||||
|
.fc-name.is-gradient {
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
}
|
||||||
|
.fc-name.is-gradient::before { -webkit-text-fill-color: initial; color: var(--text); }
|
||||||
|
|
||||||
/* tier hearts (match the old friend-name prefixes) */
|
/* tier hearts (match the old friend-name prefixes) */
|
||||||
.fc-name::before { content: "🩵 "; }
|
.fc-name::before { content: "🩵 "; }
|
||||||
.fc-name.known::before { content: "💛 "; }
|
.fc-name.known::before { content: "💛 "; }
|
||||||
|
|
@ -3530,15 +3567,20 @@ body:has(.presence-stage) {
|
||||||
padding: 0.9rem 1.4rem;
|
padding: 0.9rem 1.4rem;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
/* when a custom status sits in the identity column, top-align so the avatar
|
/* keep the identity column bottom-aligned to the avatar's visible lower
|
||||||
stays anchored to the name instead of being dragged down by the bubble */
|
half, whether or not a custom-status bubble is present */
|
||||||
.presence-stage .presence-card.has-custom .pc-head {
|
.presence-stage .presence-card.has-custom .pc-head {
|
||||||
align-items: flex-start;
|
align-items: flex-end;
|
||||||
}
|
}
|
||||||
|
/* only the avatar pokes up into the banner; the name/identity block stays
|
||||||
|
in the solid area below it */
|
||||||
.presence-stage .presence-card.has-banner .pc-head,
|
.presence-stage .presence-card.has-banner .pc-head,
|
||||||
.presence-stage .presence-card.has-banner-color .pc-head {
|
.presence-stage .presence-card.has-banner-color .pc-head {
|
||||||
/* only the avatar pokes into the banner; the name/identity block clears it */
|
margin-top: 0;
|
||||||
margin-top: -38px;
|
}
|
||||||
|
.presence-stage .presence-card.has-banner .pc-avatar,
|
||||||
|
.presence-stage .presence-card.has-banner-color .pc-avatar {
|
||||||
|
margin-top: -60px;
|
||||||
}
|
}
|
||||||
.presence-stage .pc-avatar,
|
.presence-stage .pc-avatar,
|
||||||
.presence-stage .presence-card.has-banner .pc-avatar,
|
.presence-stage .presence-card.has-banner .pc-avatar,
|
||||||
|
|
@ -3616,7 +3658,9 @@ body:has(.presence-stage) {
|
||||||
.presence-stage .presence-card.has-banner .pc-av-img,
|
.presence-stage .presence-card.has-banner .pc-av-img,
|
||||||
.presence-stage .presence-card.has-banner-color .pc-av-img { width: 92px; height: 92px; }
|
.presence-stage .presence-card.has-banner-color .pc-av-img { width: 92px; height: 92px; }
|
||||||
.presence-stage .presence-card.has-banner .pc-head,
|
.presence-stage .presence-card.has-banner .pc-head,
|
||||||
.presence-stage .presence-card.has-banner-color .pc-head { margin-top: -30px; }
|
.presence-stage .presence-card.has-banner-color .pc-head { margin-top: 0; }
|
||||||
|
.presence-stage .presence-card.has-banner .pc-avatar,
|
||||||
|
.presence-stage .presence-card.has-banner-color .pc-avatar { margin-top: -46px; }
|
||||||
.presence-stage .pc-name { font-size: 1.25rem; }
|
.presence-stage .pc-name { font-size: 1.25rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
102
js/discord.js
102
js/discord.js
|
|
@ -301,32 +301,6 @@
|
||||||
connectionsEl.hidden = false;
|
connectionsEl.hidden = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadDstn() {
|
|
||||||
fetch("https://dcdn.dstn.to/profile/" + DISCORD_USER_ID)
|
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
|
||||||
.then(function (j) {
|
|
||||||
if (!j) return;
|
|
||||||
if (Array.isArray(j.badges)) { dstnBadges = j.badges; paintBadges(); }
|
|
||||||
if (j.user_profile && Array.isArray(j.user_profile.theme_colors)) {
|
|
||||||
applyProfileGradient(j.user_profile.theme_colors);
|
|
||||||
}
|
|
||||||
const u = j.user || {};
|
|
||||||
const prof = j.user_profile || {};
|
|
||||||
applyBanner(
|
|
||||||
bannerUrl(u.id || DISCORD_USER_ID, prof.banner || u.banner),
|
|
||||||
u.banner_color || (typeof u.accent_color === "number" ? intToHex(u.accent_color) : null)
|
|
||||||
);
|
|
||||||
renderBio(prof.bio || u.bio);
|
|
||||||
renderConnections(j.connected_accounts);
|
|
||||||
if (pronounsEl) {
|
|
||||||
const pr = prof.pronouns || u.pronouns;
|
|
||||||
if (pr) { pronounsEl.textContent = pr; pronounsEl.hidden = false; }
|
|
||||||
else pronounsEl.hidden = true;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- album-art → Catppuccin accent --------------------------------------
|
// ---- album-art → Catppuccin accent --------------------------------------
|
||||||
const ACCENT_VARS = [
|
const ACCENT_VARS = [
|
||||||
"rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach",
|
"rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach",
|
||||||
|
|
@ -614,51 +588,12 @@
|
||||||
function startTicker() { if (!ticker) ticker = setInterval(updateTimes, 1000); }
|
function startTicker() { if (!ticker) ticker = setInterval(updateTimes, 1000); }
|
||||||
function stopTicker() { if (ticker) { clearInterval(ticker); ticker = null; } }
|
function stopTicker() { if (ticker) { clearInterval(ticker); ticker = null; } }
|
||||||
|
|
||||||
// ---- Lanyard websocket --------------------------------------------------
|
// ---- data source: Doughmination Restful API (sole source) ---------------
|
||||||
function connect() {
|
// Returns presence + full profile (incl. theme_colors + display_name_styles)
|
||||||
ws = new WebSocket("wss://api.lanyard.rest/socket");
|
// in a single call. Lanyard + dstn.to were removed.
|
||||||
|
|
||||||
ws.addEventListener("message", (evt) => {
|
|
||||||
let msg;
|
|
||||||
try { msg = JSON.parse(evt.data); } catch (e) { return; }
|
|
||||||
|
|
||||||
if (msg.op === 1) {
|
|
||||||
const interval = (msg.d && msg.d.heartbeat_interval) || 30000;
|
|
||||||
if (heartbeat) clearInterval(heartbeat);
|
|
||||||
heartbeat = setInterval(() => {
|
|
||||||
if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ op: 3 }));
|
|
||||||
}, interval);
|
|
||||||
ws.send(JSON.stringify({ op: 2, d: { subscribe_to_id: DISCORD_USER_ID } }));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (msg.op === 0) {
|
|
||||||
const d = msg.t === "INIT_STATE" ? (msg.d && msg.d[DISCORD_USER_ID]) || msg.d : msg.d;
|
|
||||||
render(d);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ws.addEventListener("open", () => { reconnectDelay = 1000; });
|
|
||||||
ws.addEventListener("close", () => {
|
|
||||||
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
||||||
stopTicker();
|
|
||||||
setTimeout(connect, reconnectDelay);
|
|
||||||
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
||||||
});
|
|
||||||
ws.addEventListener("error", () => { try { ws.close(); } catch (e) {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- data source: self-hosted API primary, Lanyard + dstn fallback ------
|
|
||||||
// The Doughmination Restful API returns presence + full profile in one call,
|
|
||||||
// so it drives the card on its own. We poll it; only if it can't serve us
|
|
||||||
// (network error, or no live presence) do we fail over to the Lanyard
|
|
||||||
// websocket + dstn.to, the same chain the friends grid uses.
|
|
||||||
const SELF_BASE = "https://restful.doughmination.uk/v1/users/";
|
const SELF_BASE = "https://restful.doughmination.uk/v1/users/";
|
||||||
const SELF_POLL_MS = 20000; // own-presence refresh cadence
|
const SELF_POLL_MS = 20000; // own-presence refresh cadence
|
||||||
const SELF_GIVE_UP = 3; // consecutive misses before failing over
|
|
||||||
let selfTimer = null;
|
let selfTimer = null;
|
||||||
let selfMisses = 0;
|
|
||||||
let lanyardActive = false;
|
|
||||||
|
|
||||||
// self-host shape -> the Lanyard-shaped object render() already understands
|
// self-host shape -> the Lanyard-shaped object render() already understands
|
||||||
function mapSelfHostToLanyard(j) {
|
function mapSelfHostToLanyard(j) {
|
||||||
|
|
@ -699,6 +634,9 @@
|
||||||
dstnBadges = j.data.badges;
|
dstnBadges = j.data.badges;
|
||||||
paintBadges();
|
paintBadges();
|
||||||
}
|
}
|
||||||
|
// Nitro profile gradient — straight from the self-hosted API now that it
|
||||||
|
// returns theme_colors (previously only the dstn.to fallback applied this)
|
||||||
|
if (Array.isArray(u.theme_colors)) applyProfileGradient(u.theme_colors);
|
||||||
// profile extras: banner rebuilt from the raw hash (dodges the animated
|
// profile extras: banner rebuilt from the raw hash (dodges the animated
|
||||||
// .gif 415), plus bio / connections / pronouns straight from the API
|
// .gif 415), plus bio / connections / pronouns straight from the API
|
||||||
applyBanner(
|
applyBanner(
|
||||||
|
|
@ -717,39 +655,21 @@
|
||||||
return fetch(SELF_BASE + DISCORD_USER_ID, { cache: "no-store" })
|
return fetch(SELF_BASE + DISCORD_USER_ID, { cache: "no-store" })
|
||||||
.then(function (r) { return r.ok ? r.json().catch(function () { return null; }) : null; })
|
.then(function (r) { return r.ok ? r.json().catch(function () { return null; }) : null; })
|
||||||
.then(function (j) {
|
.then(function (j) {
|
||||||
// need a live presence object to drive the card; else fall back
|
// render whenever the API has the user; presence may be null (offline)
|
||||||
if (!j || !j.success || !j.data || !j.data.user || !j.data.presence) return false;
|
if (!j || !j.success || !j.data || !j.data.user) return false;
|
||||||
renderFromSelfHost(j);
|
renderFromSelfHost(j);
|
||||||
return true;
|
return true;
|
||||||
})
|
})
|
||||||
.catch(function () { return false; });
|
.catch(function () { return false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
function startLanyardFallback() {
|
|
||||||
if (lanyardActive) return;
|
|
||||||
lanyardActive = true;
|
|
||||||
if (selfTimer) { clearInterval(selfTimer); selfTimer = null; }
|
|
||||||
connect();
|
|
||||||
loadDstn();
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollSelfHost() {
|
function pollSelfHost() {
|
||||||
if (document.hidden) return;
|
if (!document.hidden) loadSelfHosted();
|
||||||
loadSelfHosted().then(function (ok) {
|
|
||||||
if (ok) { selfMisses = 0; return; }
|
|
||||||
if (++selfMisses >= SELF_GIVE_UP) startLanyardFallback();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// boot: Doughmination first; Lanyard/dstn only if it can't serve us
|
// boot: poll the Doughmination Restful API (the only source now).
|
||||||
loadSelfHosted().then(function (ok) {
|
loadSelfHosted();
|
||||||
if (ok) {
|
|
||||||
selfMisses = 0;
|
|
||||||
selfTimer = setInterval(pollSelfHost, SELF_POLL_MS);
|
selfTimer = setInterval(pollSelfHost, SELF_POLL_MS);
|
||||||
} else {
|
|
||||||
startLanyardFallback();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", () => {
|
document.addEventListener("visibilitychange", () => {
|
||||||
if (!document.hidden && latest) updateTimes();
|
if (!document.hidden && latest) updateTimes();
|
||||||
|
|
|
||||||
154
js/friends.js
154
js/friends.js
|
|
@ -75,6 +75,55 @@
|
||||||
function intToHex(n) {
|
function intToHex(n) {
|
||||||
return "#" + (Number(n) >>> 0).toString(16).padStart(6, "0").slice(-6);
|
return "#" + (Number(n) >>> 0).toString(16).padStart(6, "0").slice(-6);
|
||||||
}
|
}
|
||||||
|
function rgbTriplet(n) {
|
||||||
|
n = Number(n) >>> 0;
|
||||||
|
return ((n >> 16) & 255) + ", " + ((n >> 8) & 255) + ", " + (n & 255);
|
||||||
|
}
|
||||||
|
// Nitro profile gradient -> card background (mirrors the /discord card)
|
||||||
|
function applyGrad(refs, colors) {
|
||||||
|
if (!colors || colors.length < 2) return;
|
||||||
|
refs.el.style.setProperty("--fc-grad-1-rgb", rgbTriplet(colors[0]));
|
||||||
|
refs.el.style.setProperty("--fc-grad-2-rgb", rgbTriplet(colors[1]));
|
||||||
|
refs.el.classList.add("has-profile-grad");
|
||||||
|
}
|
||||||
|
// 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's 8 display-name fonts -> closest free Google Fonts (approximations;
|
||||||
|
// Discord's own faces are proprietary). font_id 1 = gg sans = our default.
|
||||||
|
var FONT_BY_ID = {
|
||||||
|
2: '"Poppins", sans-serif', // Tempo
|
||||||
|
3: '"Klee One", cursive', // Sakura
|
||||||
|
4: '"Baloo 2", cursive', // Jellybean
|
||||||
|
5: '"Montserrat", sans-serif', // Modern
|
||||||
|
6: '"MedievalSharp", cursive', // Medieval
|
||||||
|
7: '"Press Start 2P", monospace', // 8Bit
|
||||||
|
8: '"Pirata One", system-ui' // Vampyre
|
||||||
|
};
|
||||||
|
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) {
|
||||||
|
if (!refs.name || !styles) return;
|
||||||
|
// gradient / colour
|
||||||
|
if (styles.colors && styles.colors.length) {
|
||||||
|
var cols = styles.colors.map(intToHex);
|
||||||
|
refs.name.style.backgroundImage = "linear-gradient(90deg, " +
|
||||||
|
(cols.length === 1 ? cols[0] + "," + cols[0] : cols.join(", ")) + ")";
|
||||||
|
refs.name.classList.add("is-gradient");
|
||||||
|
}
|
||||||
|
// font face (lazy-load the Google Fonts sheet only when one is used)
|
||||||
|
var fam = styles.font_id && FONT_BY_ID[styles.font_id];
|
||||||
|
if (fam) { ensureFonts(); 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.
|
||||||
function proxyImg(url, opts) {
|
function proxyImg(url, opts) {
|
||||||
|
|
@ -214,8 +263,21 @@
|
||||||
'</span>' +
|
'</span>' +
|
||||||
'</div>';
|
'</div>';
|
||||||
|
|
||||||
|
// Hover expansion: pin the card's current height BEFORE adding
|
||||||
|
// .is-hovering (which lifts .fc-main out of flow). Pinning keeps the
|
||||||
|
// grid slot a fixed size so the page never reflows -> no hover ping-pong.
|
||||||
|
card.addEventListener("mouseenter", function () {
|
||||||
|
card.style.height = card.offsetHeight + "px";
|
||||||
|
card.classList.add("is-hovering");
|
||||||
|
});
|
||||||
|
card.addEventListener("mouseleave", function () {
|
||||||
|
card.classList.remove("is-hovering");
|
||||||
|
card.style.height = "";
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
el: card,
|
el: card,
|
||||||
|
name: card.querySelector(".fc-name"),
|
||||||
pfp: card.querySelector(".fc-pfp"),
|
pfp: card.querySelector(".fc-pfp"),
|
||||||
deco: card.querySelector(".fc-deco"),
|
deco: card.querySelector(".fc-deco"),
|
||||||
statusDot: card.querySelector(".fc-status"),
|
statusDot: card.querySelector(".fc-status"),
|
||||||
|
|
@ -280,6 +342,8 @@
|
||||||
refs.banner.style.background = intToHex(u.accent_color);
|
refs.banner.style.background = intToHex(u.accent_color);
|
||||||
refs.el.classList.add("has-banner");
|
refs.el.classList.add("has-banner");
|
||||||
}
|
}
|
||||||
|
if (Array.isArray(u.theme_colors)) applyGrad(refs, u.theme_colors);
|
||||||
|
applyNameStyle(refs, u.display_name_styles);
|
||||||
|
|
||||||
// --- live presence ---
|
// --- live presence ---
|
||||||
if (havePresence) {
|
if (havePresence) {
|
||||||
|
|
@ -290,91 +354,25 @@
|
||||||
renderCustomStatus(refs, p.activities);
|
renderCustomStatus(refs, p.activities);
|
||||||
return "full";
|
return "full";
|
||||||
}
|
}
|
||||||
|
// User EXISTS in our API but we have no live presence for them (they're
|
||||||
|
// offline, or not in a guild our gateway tracks). Treat "exists" as
|
||||||
|
// offline rather than the default blue "unconnected". Only accounts the
|
||||||
|
// API can't find at all keep the blue dot.
|
||||||
|
refs.el.dataset.status = "offline";
|
||||||
|
refs.statusDot.title = "Offline";
|
||||||
return "profile";
|
return "profile";
|
||||||
})
|
})
|
||||||
.catch(function () { return false; });
|
.catch(function () { return false; });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- live data: Lanyard (status + avatar) ---------------------------
|
// ---- resolve a member: Doughmination Restful API ONLY ---------------
|
||||||
function loadLanyard(m, refs) {
|
// Lanyard + dstn.to were removed. Everything (presence, profile, badges,
|
||||||
return fetch("https://api.lanyard.rest/v1/users/" + m.discordId)
|
// banner, Nitro gradient, display-name styles) now comes from
|
||||||
.then(function (r) { return r.json().catch(function () { return null; }); })
|
// restful.doughmination.uk in a single call. Resolves true if the API had
|
||||||
.then(function (j) {
|
// the user, false if not found (caller stops polling after a few misses).
|
||||||
if (!j || !j.success || !j.data) {
|
|
||||||
// in config but not in the Lanyard server → stays "unconnected"
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
var d = j.data;
|
|
||||||
var u = d.discord_user || {};
|
|
||||||
var status = d.discord_status || "offline";
|
|
||||||
refs.el.dataset.status = status;
|
|
||||||
refs.statusDot.title = STATUS_TITLE[status] || "Offline";
|
|
||||||
if (u.avatar) refs.pfp.src = avatarUrl(u);
|
|
||||||
// avatar decoration (animated APNG) — loaded straight from Discord's
|
|
||||||
// CDN; the wsrv webp proxy fails on these and would drop the animation.
|
|
||||||
var deco = u.avatar_decoration_data;
|
|
||||||
if (refs.deco) {
|
|
||||||
if (deco && deco.asset) {
|
|
||||||
refs.deco.src = "https://cdn.discordapp.com/avatar-decoration-presets/" + deco.asset + ".png";
|
|
||||||
refs.deco.hidden = false;
|
|
||||||
} else {
|
|
||||||
refs.deco.hidden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (u.username) refs.user.textContent = "@" + u.username;
|
|
||||||
renderCustomStatus(refs, d.activities);
|
|
||||||
renderClanTag(refs, u.primary_guild);
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.catch(function () {
|
|
||||||
// API down / network error → static fallback, blue dot
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---- live data: dstn.to (badges + Nitro banner) ---------------------
|
|
||||||
function loadDstn(m, refs) {
|
|
||||||
return fetch("https://dcdn.dstn.to/profile/" + m.discordId)
|
|
||||||
.then(function (r) { return r.ok ? r.json() : null; })
|
|
||||||
.then(function (j) {
|
|
||||||
if (!j) return;
|
|
||||||
var u = j.user || {};
|
|
||||||
var prof = j.user_profile || {};
|
|
||||||
if (Array.isArray(j.badges) && j.badges.length) {
|
|
||||||
refs.badges.innerHTML = renderDstnBadges(j.badges);
|
|
||||||
}
|
|
||||||
var bUrl = bannerUrl(u.id || m.discordId, prof.banner || u.banner);
|
|
||||||
if (bUrl) {
|
|
||||||
refs.banner.style.backgroundImage = "url('" + bUrl + "')";
|
|
||||||
refs.el.classList.add("has-banner");
|
|
||||||
} else if (u.banner_color) {
|
|
||||||
refs.banner.style.background = u.banner_color;
|
|
||||||
refs.el.classList.add("has-banner");
|
|
||||||
} else if (typeof u.accent_color === "number") {
|
|
||||||
refs.banner.style.background = intToHex(u.accent_color);
|
|
||||||
refs.el.classList.add("has-banner");
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function () {});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Self-hosted API is the primary source; Lanyard + dstn.to are fallbacks.
|
|
||||||
// Resolves to true if ANY source had data for this member, false if they
|
|
||||||
// were not found anywhere — callers use that to stop re-polling 404s.
|
|
||||||
function refreshMember(m, refs) {
|
function refreshMember(m, refs) {
|
||||||
return loadSelfHosted(m, refs).then(function (result) {
|
return loadSelfHosted(m, refs).then(function (result) {
|
||||||
if (result === "full") return true; // self-host covered everything
|
return result !== false;
|
||||||
if (result === "profile") {
|
|
||||||
// profile came from self-host but it isn't tracking live presence —
|
|
||||||
// pull the live status dot + activity/custom status from Lanyard only.
|
|
||||||
loadLanyard(m, refs);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// result === false → self-host had nothing: original Lanyard + dstn path
|
|
||||||
return loadLanyard(m, refs).then(function (live) {
|
|
||||||
if (live) { loadDstn(m, refs); return true; }
|
|
||||||
return false; // not found in self-host OR Lanyard
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue