diff --git a/css/main.css b/css/main.css index 8f1cca2..38d1b83 100644 --- a/css/main.css +++ b/css/main.css @@ -6,7 +6,7 @@ 1. Base & reset (all pages) 2. Shared layout & header (all pages) 3. Link hub (index page) - 4. Now-playing widget (shared, top-left) + 4. Discord widget (shared, top-left) 5. Page nav (shared, bottom-left) 6. System badges (shared, bottom-right) 7. Theme switcher (shared, top-right) @@ -970,7 +970,7 @@ body:has(.friend-grid) { width: 100%; } - /* FIX: the presence card injected by now-playing.js replaces #now-playing + /* FIX: the presence card injected by discord.js replaces #discord with .presence-card (position:fixed by default in api.css). Inside .topbar on mobile it must flow normally. */ .topbar .presence-card, @@ -2215,7 +2215,7 @@ a.fc-name:hover { color: rgb(var(--accent-rgb)); } /* ===================================================================== * MUSIC PAGE (/music) — merged in from music.css. - * Hero classes are .mnp-* to avoid colliding with the .np-* now-playing + * Hero classes are .mdc-* to avoid colliding with the .dc-* discord * widget already defined above. Other classes (.lyrics, .rc-*, .sec-*, * .top-*, .ly-*, .music-*) are unique to this page. * ===================================================================== */ @@ -2260,7 +2260,7 @@ body:has(.music-wrap) { } /* ---- now playing hero -------------------------------------------------- */ -.mnp { +.mdc { display: grid; grid-template-columns: 132px 1fr; gap: 1.1rem; @@ -2275,7 +2275,7 @@ body:has(.music-wrap) { } /* a soft wash of the album accent behind the hero */ -.mnp::before { +.mdc::before { content: ""; position: absolute; inset: 0; @@ -2286,11 +2286,11 @@ body:has(.music-wrap) { pointer-events: none; } -#music.is-live .mnp::before { +#music.is-live .mdc::before { opacity: 1; } -.mnp-art { +.mdc-art { width: 132px; height: 132px; border-radius: 12px; @@ -2299,11 +2299,11 @@ body:has(.music-wrap) { box-shadow: 0 8px 24px rgba(0, 0, 0, 0.35); } -.mnp-art:not(.has-art) { +.mdc-art:not(.has-art) { display: grid; } -.mnp-art:not(.has-art)::after { +.mdc-art:not(.has-art)::after { content: "♪"; color: var(--overlay-0); font-size: 2.4rem; @@ -2312,12 +2312,12 @@ body:has(.music-wrap) { height: 100%; } -.mnp-meta { +.mdc-meta { min-width: 0; position: relative; } -.mnp-state { +.mdc-state { display: inline-flex; align-items: center; gap: 0.4rem; @@ -2328,11 +2328,11 @@ body:has(.music-wrap) { margin-bottom: 0.35rem; } -#music.is-live .mnp-state { +#music.is-live .mdc-state { color: rgb(var(--accent-rgb)); } -.mnp-title { +.mdc-title { display: block; font-size: 1.3rem; font-weight: 700; @@ -2344,31 +2344,31 @@ body:has(.music-wrap) { text-overflow: ellipsis; } -.mnp-title:hover { +.mdc-title:hover { color: rgb(var(--accent-rgb)); } -.mnp-artist { +.mdc-artist { display: block; color: var(--subtext-1); font-size: 0.95rem; } -.mnp-album { +.mdc-album { display: block; color: var(--subtext-0); font-size: 0.82rem; margin-top: 0.1rem; } -.mnp-progress { +.mdc-progress { margin-top: 0.8rem; display: flex; align-items: center; gap: 0.6rem; } -.mnp-bar { +.mdc-bar { flex: 1; height: 6px; border-radius: 999px; @@ -2376,7 +2376,7 @@ body:has(.music-wrap) { overflow: hidden; } -.mnp-fill { +.mdc-fill { display: block; height: 100%; width: 0%; @@ -2385,7 +2385,7 @@ body:has(.music-wrap) { transition: width 0.4s linear; } -.mnp-time { +.mdc-time { font-size: 0.72rem; color: var(--subtext-0); font-variant-numeric: tabular-nums; @@ -2719,18 +2719,18 @@ body:has(.music-wrap) { /* ---- responsive -------------------------------------------------------- */ @media (max-width: 560px) { - .mnp { + .mdc { grid-template-columns: 96px 1fr; gap: 0.85rem; padding: 0.9rem; } - .mnp-art { + .mdc-art { width: 96px; height: 96px; } - .mnp-title { + .mdc-title { font-size: 1.1rem; } @@ -2744,7 +2744,7 @@ body:has(.music-wrap) { } @media (prefers-reduced-motion: reduce) { - .mnp-fill { + .mdc-fill { transition: none; } @@ -2906,7 +2906,7 @@ html[data-theme] body.api-body { * Lifted verbatim from main.css §13 so the card is self-contained. * ===================================================================== */ .presence-card { - --np-accent: 245, 194, 231; + --dc-accent: 245, 194, 231; position: fixed; top: 1rem; left: 1rem; @@ -2924,8 +2924,8 @@ html[data-theme] body.api-body { .presence-card[hidden] { display: none; } .presence-card.has-accent { - border-color: rgba(var(--np-accent), 0.5); - box-shadow: 0 8px 26px -12px rgba(var(--np-accent), 0.6); + border-color: rgba(var(--dc-accent), 0.5); + box-shadow: 0 8px 26px -12px rgba(var(--dc-accent), 0.6); } /* ---- header (always visible) ---- */ @@ -3054,7 +3054,7 @@ html[data-theme] body.api-body { } a.pc-row:hover, .pc-row--stack:hover { - border-color: rgba(var(--np-accent), 0.55); + border-color: rgba(var(--dc-accent), 0.55); transform: translateX(2px); } @@ -3171,7 +3171,7 @@ a.pc-row:hover, height: 100%; width: 0; border-radius: 999px; - background: rgb(var(--np-accent)); + background: rgb(var(--dc-accent)); } .pc-times { display: flex; @@ -3298,7 +3298,7 @@ a.pc-row:hover, transition: border-color 0.15s ease, background 0.15s ease; } .pc-btn:hover { - border-color: rgb(var(--np-accent)); + border-color: rgb(var(--dc-accent)); background: var(--surface-2); } diff --git a/discord/index.html b/discord/index.html index d78d159..caa2db0 100644 --- a/discord/index.html +++ b/discord/index.html @@ -61,11 +61,11 @@

What fae is up to, live via Lanyard.

-
+
- + diff --git a/js/core.js b/js/core.js index defbe0f..729c72f 100644 --- a/js/core.js +++ b/js/core.js @@ -89,7 +89,7 @@ document.querySelectorAll("[data-href]").forEach((el) => { `; - // Group the single-item widgets (now-playing + theme toggle) into one + // Group the single-item widgets (discord + theme toggle) into one // top bar. On mobile they sit side by side; on desktop both stay // position:fixed, so this wrapper is zero-size and invisible. let topbar = document.querySelector(".topbar"); @@ -97,10 +97,10 @@ document.querySelectorAll("[data-href]").forEach((el) => { topbar = document.createElement("div"); topbar.className = "topbar"; document.body.insertBefore(topbar, document.body.firstChild); - const np = document.getElementById("now-playing"); + const dc = document.getElementById("discord"); // Don't hijack the presence card when it's the centerpiece of the // dedicated /discord page (it lives inside .presence-stage there). - if (np && !np.closest(".presence-stage")) topbar.appendChild(np); + if (dc && !dc.closest(".presence-stage")) topbar.appendChild(dc); } topbar.appendChild(bar); @@ -676,16 +676,16 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE; }); /* ---------- Gold → opened the site while Discord status is Idle ---------- */ - const np = document.getElementById("now-playing"); - if (np) { + const dc = document.getElementById("discord"); + if (dc) { const checkIdle = () => { - if (np.dataset.status === "idle" && unlockMethod("gold")) { + if (dc.dataset.status === "idle" && unlockMethod("gold")) { toast("✨ Gold Cat unlocked!"); } }; checkIdle(); new MutationObserver(checkIdle) - .observe(np, { attributes: true, attributeFilter: ["data-status"] }); + .observe(dc, { attributes: true, attributeFilter: ["data-status"] }); } /* ---------- Pokémon → find & click the hidden pokéball ---------- */ diff --git a/js/now-playing.js b/js/discord.js similarity index 98% rename from js/now-playing.js rename to js/discord.js index 75536cd..da6cb6d 100644 --- a/js/now-playing.js +++ b/js/discord.js @@ -12,14 +12,14 @@ if (/^#\d{5,25}$/.test(location.hash)) return location.hash.slice(1); const script = document.currentScript || document.querySelector("script[data-user]"); if (script && script.dataset && valid(script.dataset.user)) return script.dataset.user; - const m = document.getElementById("now-playing"); + const m = document.getElementById("discord"); if (m && m.dataset && valid(m.dataset.user)) return m.dataset.user; - if (valid(window.NOW_PLAYING_USER_ID)) return window.NOW_PLAYING_USER_ID; + if (valid(window.DISCORD_USER_ID)) return window.DISCORD_USER_ID; return null; } const DISCORD_USER_ID = resolveUserId(); - const mount = document.getElementById("now-playing"); + const mount = document.getElementById("discord"); if (!mount || !DISCORD_USER_ID) return; // ---- theme: only on standalone api pages (homepage uses data-flavor) ---- @@ -33,7 +33,7 @@ // ---- build the card ----------------------------------------------------- const card = document.createElement("div"); - card.id = "now-playing"; + card.id = "discord"; card.className = "presence-card"; card.hidden = true; card.innerHTML = @@ -382,7 +382,7 @@ r = Math.round(r / count); g = Math.round(g / count); b = Math.round(b / count); const near = nearestAccent(r, g, b); const rgb = near ? `${near.r}, ${near.g}, ${near.b}` : `${r}, ${g}, ${b}`; - card.style.setProperty("--np-accent", rgb); + card.style.setProperty("--dc-accent", rgb); card.classList.add("has-accent"); document.documentElement.style.setProperty("--accent-rgb", rgb); } catch (e) { resetAccent(); } @@ -393,7 +393,7 @@ function resetAccent() { lastArtUrl = null; card.classList.remove("has-accent"); - card.style.removeProperty("--np-accent"); + card.style.removeProperty("--dc-accent"); document.documentElement.style.removeProperty("--accent-rgb"); } diff --git a/js/friends.js b/js/friends.js index 91520b2..3f2ad35 100644 --- a/js/friends.js +++ b/js/friends.js @@ -11,6 +11,7 @@ { title: "Close Friends", members: [ + { name: "Lilly", img: "/assets/friends/lilly.png", tier: "close", discordId: "908055723659898902", link: null }, { name: "Ria", img: "/assets/friends/ria.png", tier: "close", discordId: "1513506390088618145", link: null }, { name: "Camilla", img: "/assets/friends/camilla.png", tier: "close", discordId: "1110542429838397471", link: "https://cammy-the-cat.com" }, { name: "Saphie", img: "/assets/friends/saphie.png", tier: "close", discordId: "527709099186716673", link: null }, @@ -66,7 +67,7 @@ .replace(/^-+|-+$/g, ""); } - // ---- helpers (mirrors now-playing.js) ------------------------------- + // ---- helpers (mirrors discord.js) ----------------------------------- function esc(str) { return String(str == null ? "" : str) .replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); @@ -140,6 +141,34 @@ }).join(""); } + // Self-hosted badge list → small icon row. The self-hosted API ships a + // ready-made `icon_url`; fall back to building it from `icon` if absent. + function renderSelfBadges(badges) { + if (!Array.isArray(badges)) return ""; + return badges.map(function (b) { + var url = b.icon_url || ("https://cdn.discordapp.com/badge-icons/" + esc(b.icon) + ".png"); + var img = '' + esc(b.description || b.id) + ''; + return b.link + ? '' + img + "" + : img; + }).join(""); + } + + // Self-hosted clan tag → guild badge + tag. Shape: {tag, badge_url}. + function renderSelfClan(refs, clan) { + if (!refs.tag) return; + if (clan && clan.tag) { + var b = clan.badge_url ? proxyImg(clan.badge_url) : null; + refs.tag.innerHTML = (b ? '' : "") + + '' + esc(clan.tag) + ""; + refs.tag.hidden = false; + } else { + refs.tag.hidden = true; + } + } + // Discord server (clan) tag — the little guild badge + tag next to a name function guildTagBadgeUrl(pg) { if (!pg || !pg.badge || !pg.identity_guild_id) return null; @@ -205,6 +234,67 @@ offline: "Offline" }; + // ---- live data: self-hosted API (primary source) ------------------- + // Returns a promise resolving to: + // "full" → user + live presence rendered (no fallback needed) + // "profile" → user rendered but presence was null (fall back to Lanyard + // for the live status dot + current activity/custom status) + // false → nothing usable (full Lanyard + dstn fallback) + var SELF_BASE = "https://restful.doughmination.uk/v1/users/"; + + function loadSelfHosted(m, refs) { + return fetch(SELF_BASE + m.discordId, { cache: "no-store" }) + .then(function (r) { return r.ok ? r.json().catch(function () { return null; }) : null; }) + .then(function (j) { + if (!j || !j.success || !j.data || !j.data.user) return false; + var u = j.data.user || {}; + var p = j.data.presence; // may be null when the user isn't live-tracked + var havePresence = !!p; + + // --- profile (rendered whenever we have a user object) --- + if (u.avatar) { + refs.pfp.src = avatarUrl({ id: u.id || m.discordId, avatar: u.avatar }); + } + // avatar decoration — load straight from Discord's CDN (the wsrv webp + // proxy drops the APNG animation). API ships a ready `url`. + if (refs.deco) { + var deco = u.avatar_decoration; + var dUrl = deco && (deco.url || + (deco.asset ? "https://cdn.discordapp.com/avatar-decoration-presets/" + deco.asset + ".png" : null)); + if (dUrl) { refs.deco.src = dUrl; refs.deco.hidden = false; } + else { refs.deco.hidden = true; } + } + if (u.username) refs.user.textContent = "@" + u.username; + renderSelfClan(refs, u.clan); + if (Array.isArray(j.data.badges) && j.data.badges.length) { + refs.badges.innerHTML = renderSelfBadges(j.data.badges); + } + // banner — rebuild from the raw hash via bannerUrl() rather than the + // API's banner_url, which uses a `.gif` extension that 415s for some + // animated banners. Falls back to accent colour. + var bUrl = bannerUrl(u.id || m.discordId, u.banner); + if (bUrl) { + refs.banner.style.backgroundImage = "url('" + bUrl + "')"; + 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"); + } + + // --- live presence --- + if (havePresence) { + var status = p.status || (p.online ? "online" : "offline"); + refs.el.dataset.status = status; + refs.statusDot.title = STATUS_TITLE[status] || "Offline"; + // activities follow the standard Discord shape (type 4 = custom status) + renderCustomStatus(refs, p.activities); + return "full"; + } + return "profile"; + }) + .catch(function () { return false; }); + } + // ---- live data: Lanyard (status + avatar) --------------------------- function loadLanyard(m, refs) { return fetch("https://api.lanyard.rest/v1/users/" + m.discordId) @@ -268,9 +358,38 @@ .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) { - loadLanyard(m, refs).then(function (live) { - if (live) loadDstn(m, refs); + return loadSelfHosted(m, refs).then(function (result) { + if (result === "full") return true; // self-host covered everything + 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 + }); + }); + } + + // Stop polling a member after this many consecutive "not found anywhere" + // results. Keeps 404s (e.g. someone not in the self-host DB or Lanyard) + // from spamming the console on every refresh. Any successful resolve resets + // the counter, so a transient blip won't permanently drop a friend. Tunable. + var GIVE_UP_AFTER = 3; + + function pollEntry(entry) { + if (entry.stop) return; + refreshMember(entry.m, entry.refs).then(function (resolved) { + if (resolved) { entry.misses = 0; return; } + entry.misses++; + if (entry.misses >= GIVE_UP_AFTER) entry.stop = true; // give up for this session }); } @@ -302,8 +421,9 @@ grid.appendChild(refs.el); // dead alts are banned/retired — never pull live data for them if (m.discordId && m.tier !== "dead-alt") { - liveMembers.push({ m: m, refs: refs }); - refreshMember(m, refs); + var entry = { m: m, refs: refs, misses: 0, stop: false }; + liveMembers.push(entry); + pollEntry(entry); } }); @@ -325,7 +445,7 @@ if (liveMembers.length) { setInterval(function () { if (document.hidden) return; - liveMembers.forEach(function (x) { refreshMember(x.m, x.refs); }); + liveMembers.forEach(function (x) { if (!x.stop) pollEntry(x); }); }, REFRESH_MS); } })(); diff --git a/js/music.js b/js/music.js index 93318cd..d09c580 100644 --- a/js/music.js +++ b/js/music.js @@ -33,7 +33,7 @@ } function clamp(n, lo, hi) { return n < lo ? lo : n > hi ? hi : n; } - // ---- album art → Catppuccin accent (same maths as now-playing.js) ------- + // ---- album art → Catppuccin accent (same maths as discord.js) ------- const ACCENT_VARS = [ "rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "yellow", "green", "teal", "sky", "saphire", "blue", "lavender", @@ -103,22 +103,22 @@ // ---- DOM refs ----------------------------------------------------------- const stage = $("#music"); if (!stage) return; - const npArt = $("#np-art"); - const npState = $("#np-state"); - const npTitle = $("#np-title"); - const npArtist = $("#np-artist"); - const npAlbum = $("#np-album"); - const npLink = $("#np-link"); - const barFill = $("#np-fill"); - const barCur = $("#np-cur"); - const barDur = $("#np-dur"); - const progress = $("#np-progress"); + const dcArt = $("#dc-art"); + const dcState = $("#dc-state"); + const dcTitle = $("#dc-title"); + const dcArtist = $("#dc-artist"); + const dcAlbum = $("#dc-album"); + const dcLink = $("#dc-link"); + const barFill = $("#dc-fill"); + const barCur = $("#dc-cur"); + const barDur = $("#dc-dur"); + const progress = $("#dc-progress"); const lyricsBox = $("#lyrics"); const lockBtn = $("#ly-lock"); const recentBox = $("#recent"); const topBox = $("#top"); // transparent 1x1 — keeps valid (src required) while showing the ♪ - // placeholder via CSS (.mnp-art:not(.has-art)) when there's no real art + // placeholder via CSS (.mdc-art:not(.has-art)) when there's no real art const BLANK_ART = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; // ---- state -------------------------------------------------------------- @@ -140,29 +140,29 @@ function paintHero() { if (!track) { stage.classList.add("is-idle"); - npState.textContent = "Not listening right now"; - npTitle.textContent = "—"; - npArtist.textContent = ""; - npAlbum.textContent = ""; - npArt.src = BLANK_ART; - npArt.classList.remove("has-art"); - npLink.removeAttribute("href"); - npLink.removeAttribute("target"); - npLink.removeAttribute("rel"); + dcState.textContent = "Not listening right now"; + dcTitle.textContent = "—"; + dcArtist.textContent = ""; + dcAlbum.textContent = ""; + dcArt.src = BLANK_ART; + dcArt.classList.remove("has-art"); + dcLink.removeAttribute("href"); + dcLink.removeAttribute("target"); + dcLink.removeAttribute("rel"); progress.hidden = true; resetAccent(); return; } stage.classList.toggle("is-idle", false); stage.classList.toggle("is-live", !!track.live); - npState.textContent = track.live ? "Listening now" : "Last played"; - npTitle.textContent = track.song || "Unknown track"; - npArtist.textContent = track.artist || ""; - npAlbum.textContent = track.album || ""; - if (track.art) { npArt.src = track.art; npArt.classList.add("has-art"); } - else { npArt.src = BLANK_ART; npArt.classList.remove("has-art"); } - if (track.url) { npLink.href = track.url; npLink.target = "_blank"; npLink.rel = "noopener"; } - else { npLink.removeAttribute("href"); npLink.removeAttribute("target"); npLink.removeAttribute("rel"); } + dcState.textContent = track.live ? "Listening now" : "Last played"; + dcTitle.textContent = track.song || "Unknown track"; + dcArtist.textContent = track.artist || ""; + dcAlbum.textContent = track.album || ""; + if (track.art) { dcArt.src = track.art; dcArt.classList.add("has-art"); } + else { dcArt.src = BLANK_ART; dcArt.classList.remove("has-art"); } + if (track.url) { dcLink.href = track.url; dcLink.target = "_blank"; dcLink.rel = "noopener"; } + else { dcLink.removeAttribute("href"); dcLink.removeAttribute("target"); dcLink.removeAttribute("rel"); } // progress bar only makes sense for a live track with real timestamps progress.hidden = !(track.live && track.start && track.end); if (!progress.hidden) barDur.textContent = mmss(track.duration); diff --git a/music/index.html b/music/index.html index 098d704..17d418b 100644 --- a/music/index.html +++ b/music/index.html @@ -47,17 +47,17 @@ - - -
- Connecting… - - - -