This commit is contained in:
Clove 2026-06-19 01:39:44 +01:00
parent 10f06dc497
commit 84868aba45
7 changed files with 211 additions and 91 deletions

View File

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

View File

@ -61,11 +61,11 @@
<p>What fae is up to, live via Lanyard.</p> <p>What fae is up to, live via Lanyard.</p>
</div> </div>
<div id="now-playing"></div> <div id="discord"></div>
</main> </main>
<script src="/js/core.js" data-cat="/assets/oneko/classics/classic.png"></script> <script src="/js/core.js" data-cat="/assets/oneko/classics/classic.png"></script>
<script src="/js/now-playing.js" data-user="1464890289922641993"></script> <script src="/js/discord.js" data-user="1464890289922641993"></script>
</body> </body>
</html> </html>

View File

@ -89,7 +89,7 @@ document.querySelectorAll("[data-href]").forEach((el) => {
<button class="beta-btn" id="flavor-btn" type="button"> <button class="beta-btn" id="flavor-btn" type="button">
<img class="beta-icon" alt=""> <img class="beta-icon" alt="">
</button>`; </button>`;
// 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 // top bar. On mobile they sit side by side; on desktop both stay
// position:fixed, so this wrapper is zero-size and invisible. // position:fixed, so this wrapper is zero-size and invisible.
let topbar = document.querySelector(".topbar"); let topbar = document.querySelector(".topbar");
@ -97,10 +97,10 @@ document.querySelectorAll("[data-href]").forEach((el) => {
topbar = document.createElement("div"); topbar = document.createElement("div");
topbar.className = "topbar"; topbar.className = "topbar";
document.body.insertBefore(topbar, document.body.firstChild); 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 // Don't hijack the presence card when it's the centerpiece of the
// dedicated /discord page (it lives inside .presence-stage there). // 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); topbar.appendChild(bar);
@ -676,16 +676,16 @@ const spriteFor = (c) => c.sprite || BASE_SPRITE;
}); });
/* ---------- Gold → opened the site while Discord status is Idle ---------- */ /* ---------- Gold → opened the site while Discord status is Idle ---------- */
const np = document.getElementById("now-playing"); const dc = document.getElementById("discord");
if (np) { if (dc) {
const checkIdle = () => { const checkIdle = () => {
if (np.dataset.status === "idle" && unlockMethod("gold")) { if (dc.dataset.status === "idle" && unlockMethod("gold")) {
toast("✨ Gold Cat unlocked!"); toast("✨ Gold Cat unlocked!");
} }
}; };
checkIdle(); checkIdle();
new MutationObserver(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 ---------- */ /* ---------- Pokémon → find & click the hidden pokéball ---------- */

View File

@ -12,14 +12,14 @@
if (/^#\d{5,25}$/.test(location.hash)) return location.hash.slice(1); if (/^#\d{5,25}$/.test(location.hash)) return location.hash.slice(1);
const script = document.currentScript || document.querySelector("script[data-user]"); const script = document.currentScript || document.querySelector("script[data-user]");
if (script && script.dataset && valid(script.dataset.user)) return script.dataset.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 (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; return null;
} }
const DISCORD_USER_ID = resolveUserId(); const DISCORD_USER_ID = resolveUserId();
const mount = document.getElementById("now-playing"); const mount = document.getElementById("discord");
if (!mount || !DISCORD_USER_ID) return; if (!mount || !DISCORD_USER_ID) return;
// ---- theme: only on standalone api pages (homepage uses data-flavor) ---- // ---- theme: only on standalone api pages (homepage uses data-flavor) ----
@ -33,7 +33,7 @@
// ---- build the card ----------------------------------------------------- // ---- build the card -----------------------------------------------------
const card = document.createElement("div"); const card = document.createElement("div");
card.id = "now-playing"; card.id = "discord";
card.className = "presence-card"; card.className = "presence-card";
card.hidden = true; card.hidden = true;
card.innerHTML = card.innerHTML =
@ -382,7 +382,7 @@
r = Math.round(r / count); g = Math.round(g / count); b = Math.round(b / count); r = Math.round(r / count); g = Math.round(g / count); b = Math.round(b / count);
const near = nearestAccent(r, g, b); const near = nearestAccent(r, g, b);
const rgb = near ? `${near.r}, ${near.g}, ${near.b}` : `${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"); card.classList.add("has-accent");
document.documentElement.style.setProperty("--accent-rgb", rgb); document.documentElement.style.setProperty("--accent-rgb", rgb);
} catch (e) { resetAccent(); } } catch (e) { resetAccent(); }
@ -393,7 +393,7 @@
function resetAccent() { function resetAccent() {
lastArtUrl = null; lastArtUrl = null;
card.classList.remove("has-accent"); card.classList.remove("has-accent");
card.style.removeProperty("--np-accent"); card.style.removeProperty("--dc-accent");
document.documentElement.style.removeProperty("--accent-rgb"); document.documentElement.style.removeProperty("--accent-rgb");
} }

View File

@ -11,6 +11,7 @@
{ {
title: "Close Friends", title: "Close Friends",
members: [ 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: "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: "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 }, { name: "Saphie", img: "/assets/friends/saphie.png", tier: "close", discordId: "527709099186716673", link: null },
@ -66,7 +67,7 @@
.replace(/^-+|-+$/g, ""); .replace(/^-+|-+$/g, "");
} }
// ---- helpers (mirrors now-playing.js) ------------------------------- // ---- helpers (mirrors discord.js) -----------------------------------
function esc(str) { function esc(str) {
return String(str == null ? "" : str) return String(str == null ? "" : str)
.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;"); .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
@ -140,6 +141,34 @@
}).join(""); }).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 = '<img class="fc-badge" src="' + proxyImg(url) +
'" alt="' + esc(b.description || b.id) + '" title="' + esc(b.description || b.id) +
'" onerror="this.remove()">';
return b.link
? '<a class="fc-badge-link" href="' + esc(b.link) + '" target="_blank" rel="noopener">' + img + "</a>"
: 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 ? '<img class="fc-tag-badge" src="' + b + '" alt="" onerror="this.remove()">' : "") +
'<span class="fc-tag-text">' + esc(clan.tag) + "</span>";
refs.tag.hidden = false;
} else {
refs.tag.hidden = true;
}
}
// Discord server (clan) tag — the little guild badge + tag next to a name // Discord server (clan) tag — the little guild badge + tag next to a name
function guildTagBadgeUrl(pg) { function guildTagBadgeUrl(pg) {
if (!pg || !pg.badge || !pg.identity_guild_id) return null; if (!pg || !pg.badge || !pg.identity_guild_id) return null;
@ -205,6 +234,67 @@
offline: "Offline" 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) --------------------------- // ---- live data: Lanyard (status + avatar) ---------------------------
function loadLanyard(m, refs) { function loadLanyard(m, refs) {
return fetch("https://api.lanyard.rest/v1/users/" + m.discordId) return fetch("https://api.lanyard.rest/v1/users/" + m.discordId)
@ -268,9 +358,38 @@
.catch(function () {}); .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) {
loadLanyard(m, refs).then(function (live) { return loadSelfHosted(m, refs).then(function (result) {
if (live) loadDstn(m, refs); 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); grid.appendChild(refs.el);
// dead alts are banned/retired — never pull live data for them // dead alts are banned/retired — never pull live data for them
if (m.discordId && m.tier !== "dead-alt") { if (m.discordId && m.tier !== "dead-alt") {
liveMembers.push({ m: m, refs: refs }); var entry = { m: m, refs: refs, misses: 0, stop: false };
refreshMember(m, refs); liveMembers.push(entry);
pollEntry(entry);
} }
}); });
@ -325,7 +445,7 @@
if (liveMembers.length) { if (liveMembers.length) {
setInterval(function () { setInterval(function () {
if (document.hidden) return; if (document.hidden) return;
liveMembers.forEach(function (x) { refreshMember(x.m, x.refs); }); liveMembers.forEach(function (x) { if (!x.stop) pollEntry(x); });
}, REFRESH_MS); }, REFRESH_MS);
} }
})(); })();

View File

@ -33,7 +33,7 @@
} }
function clamp(n, lo, hi) { return n < lo ? lo : n > hi ? hi : n; } 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 = [ const ACCENT_VARS = [
"rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach", "rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach",
"yellow", "green", "teal", "sky", "saphire", "blue", "lavender", "yellow", "green", "teal", "sky", "saphire", "blue", "lavender",
@ -103,22 +103,22 @@
// ---- DOM refs ----------------------------------------------------------- // ---- DOM refs -----------------------------------------------------------
const stage = $("#music"); const stage = $("#music");
if (!stage) return; if (!stage) return;
const npArt = $("#np-art"); const dcArt = $("#dc-art");
const npState = $("#np-state"); const dcState = $("#dc-state");
const npTitle = $("#np-title"); const dcTitle = $("#dc-title");
const npArtist = $("#np-artist"); const dcArtist = $("#dc-artist");
const npAlbum = $("#np-album"); const dcAlbum = $("#dc-album");
const npLink = $("#np-link"); const dcLink = $("#dc-link");
const barFill = $("#np-fill"); const barFill = $("#dc-fill");
const barCur = $("#np-cur"); const barCur = $("#dc-cur");
const barDur = $("#np-dur"); const barDur = $("#dc-dur");
const progress = $("#np-progress"); const progress = $("#dc-progress");
const lyricsBox = $("#lyrics"); const lyricsBox = $("#lyrics");
const lockBtn = $("#ly-lock"); const lockBtn = $("#ly-lock");
const recentBox = $("#recent"); const recentBox = $("#recent");
const topBox = $("#top"); const topBox = $("#top");
// transparent 1x1 — keeps <img> valid (src required) while showing the ♪ // transparent 1x1 — keeps <img> 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"; const BLANK_ART = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
// ---- state -------------------------------------------------------------- // ---- state --------------------------------------------------------------
@ -140,29 +140,29 @@
function paintHero() { function paintHero() {
if (!track) { if (!track) {
stage.classList.add("is-idle"); stage.classList.add("is-idle");
npState.textContent = "Not listening right now"; dcState.textContent = "Not listening right now";
npTitle.textContent = "—"; dcTitle.textContent = "—";
npArtist.textContent = ""; dcArtist.textContent = "";
npAlbum.textContent = ""; dcAlbum.textContent = "";
npArt.src = BLANK_ART; dcArt.src = BLANK_ART;
npArt.classList.remove("has-art"); dcArt.classList.remove("has-art");
npLink.removeAttribute("href"); dcLink.removeAttribute("href");
npLink.removeAttribute("target"); dcLink.removeAttribute("target");
npLink.removeAttribute("rel"); dcLink.removeAttribute("rel");
progress.hidden = true; progress.hidden = true;
resetAccent(); resetAccent();
return; return;
} }
stage.classList.toggle("is-idle", false); stage.classList.toggle("is-idle", false);
stage.classList.toggle("is-live", !!track.live); stage.classList.toggle("is-live", !!track.live);
npState.textContent = track.live ? "Listening now" : "Last played"; dcState.textContent = track.live ? "Listening now" : "Last played";
npTitle.textContent = track.song || "Unknown track"; dcTitle.textContent = track.song || "Unknown track";
npArtist.textContent = track.artist || ""; dcArtist.textContent = track.artist || "";
npAlbum.textContent = track.album || ""; dcAlbum.textContent = track.album || "";
if (track.art) { npArt.src = track.art; npArt.classList.add("has-art"); } if (track.art) { dcArt.src = track.art; dcArt.classList.add("has-art"); }
else { npArt.src = BLANK_ART; npArt.classList.remove("has-art"); } else { dcArt.src = BLANK_ART; dcArt.classList.remove("has-art"); }
if (track.url) { npLink.href = track.url; npLink.target = "_blank"; npLink.rel = "noopener"; } if (track.url) { dcLink.href = track.url; dcLink.target = "_blank"; dcLink.rel = "noopener"; }
else { npLink.removeAttribute("href"); npLink.removeAttribute("target"); npLink.removeAttribute("rel"); } else { dcLink.removeAttribute("href"); dcLink.removeAttribute("target"); dcLink.removeAttribute("rel"); }
// progress bar only makes sense for a live track with real timestamps // progress bar only makes sense for a live track with real timestamps
progress.hidden = !(track.live && track.start && track.end); progress.hidden = !(track.live && track.start && track.end);
if (!progress.hidden) barDur.textContent = mmss(track.duration); if (!progress.hidden) barDur.textContent = mmss(track.duration);

View File

@ -47,17 +47,17 @@
</header> </header>
<!-- now playing --> <!-- now playing -->
<a class="mnp" id="np-link"> <a class="mdc" id="dc-link">
<img class="mnp-art" id="np-art" alt="" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"> <img class="mdc-art" id="dc-art" alt="" src="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7">
<div class="mnp-meta"> <div class="mdc-meta">
<span class="mnp-state" id="np-state">Connecting…</span> <span class="mdc-state" id="dc-state">Connecting…</span>
<span class="mnp-title" id="np-title"></span> <span class="mdc-title" id="dc-title"></span>
<span class="mnp-artist" id="np-artist"></span> <span class="mdc-artist" id="dc-artist"></span>
<span class="mnp-album" id="np-album"></span> <span class="mdc-album" id="dc-album"></span>
<div class="mnp-progress" id="np-progress" hidden> <div class="mdc-progress" id="dc-progress" hidden>
<span class="mnp-time" id="np-cur">0:00</span> <span class="mdc-time" id="dc-cur">0:00</span>
<span class="mnp-bar"><span class="mnp-fill" id="np-fill"></span></span> <span class="mdc-bar"><span class="mdc-fill" id="dc-fill"></span></span>
<span class="mnp-time" id="np-dur">0:00</span> <span class="mdc-time" id="dc-dur">0:00</span>
</div> </div>
</div> </div>
</a> </a>