new api
This commit is contained in:
parent
10f06dc497
commit
84868aba45
60
css/main.css
60
css/main.css
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
14
js/core.js
14
js/core.js
|
|
@ -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 ---------- */
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
||||||
132
js/friends.js
132
js/friends.js
|
|
@ -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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
58
js/music.js
58
js/music.js
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue