256 lines
8.5 KiB
JavaScript
256 lines
8.5 KiB
JavaScript
const DISCORD_USER_ID = "1464890289922641993";
|
|
|
|
(function nowPlaying() {
|
|
const el = document.getElementById("now-playing");
|
|
if (!el) return;
|
|
|
|
// Stay hidden until configured (keeps the widget invisible on a fresh clone)
|
|
if (!DISCORD_USER_ID || DISCORD_USER_ID === "REPLACE_WITH_YOUR_DISCORD_USER_ID") {
|
|
return;
|
|
}
|
|
|
|
const artEl = el.querySelector(".np-art");
|
|
const labelEl = el.querySelector(".np-label");
|
|
const statusLabelEl = el.querySelector(".np-status-label");
|
|
const trackEl = el.querySelector(".np-track");
|
|
const artistEl = el.querySelector(".np-artist");
|
|
const fillEl = el.querySelector(".np-fill");
|
|
const curEl = el.querySelector(".np-cur");
|
|
const durEl = el.querySelector(".np-dur");
|
|
|
|
const STATUS_LABELS = {
|
|
online: "Online",
|
|
idle: "Idle",
|
|
dnd: "Do Not Disturb",
|
|
offline: "Offline",
|
|
};
|
|
|
|
let latest = null; // last presence payload
|
|
let progressTimer = null; // 1s ticker while a track is playing
|
|
let ws = null;
|
|
let heartbeat = null;
|
|
let reconnectDelay = 1000;
|
|
|
|
function fmt(ms) {
|
|
const total = Math.max(0, Math.floor(ms / 1000));
|
|
const m = Math.floor(total / 60);
|
|
const s = total % 60;
|
|
return `${m}:${String(s).padStart(2, "0")}`;
|
|
}
|
|
|
|
function clamp(n, lo, hi) {
|
|
return Math.min(Math.max(n, lo), hi);
|
|
}
|
|
|
|
// ---- snap album-art colour to the active Catppuccin palette ------------
|
|
// The accent vars are read live from CSS, so this follows whichever flavour
|
|
// (mocha / macchiato / frappe / latte) is currently active.
|
|
const ACCENT_VARS = [
|
|
"rosewater", "flamingo", "pink", "mauve", "red", "maroon", "peach",
|
|
"yellow", "green", "teal", "sky", "saphire", "blue", "lavender",
|
|
];
|
|
|
|
function hexToRgb(hex) {
|
|
hex = hex.trim().replace("#", "");
|
|
if (hex.length === 3) hex = hex.split("").map((c) => c + c).join("");
|
|
const n = parseInt(hex, 16);
|
|
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
}
|
|
|
|
function getThemePalette() {
|
|
const cs = getComputedStyle(document.documentElement);
|
|
const pal = [];
|
|
for (const name of ACCENT_VARS) {
|
|
const v = cs.getPropertyValue("--" + name).trim();
|
|
if (v.startsWith("#")) {
|
|
const [r, g, b] = hexToRgb(v);
|
|
pal.push({ name, r, g, b });
|
|
}
|
|
}
|
|
return pal;
|
|
}
|
|
|
|
// Nearest palette swatch using a redmean-weighted distance (closer to how
|
|
// the eye judges colour difference than plain RGB distance).
|
|
function nearestAccent(r, g, b) {
|
|
const pal = getThemePalette();
|
|
let best = null, bestD = Infinity;
|
|
for (const c of pal) {
|
|
const rm = (r + c.r) / 2;
|
|
const dr = r - c.r, dg = g - c.g, db = b - c.b;
|
|
const d = (2 + rm / 256) * dr * dr + 4 * dg * dg + (2 + (255 - rm) / 256) * db * db;
|
|
if (d < bestD) { bestD = d; best = c; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
// ---- album-art accent colour -------------------------------------------
|
|
let lastArtUrl = null;
|
|
function applyAccent(url) {
|
|
if (!url || url === lastArtUrl) return;
|
|
lastArtUrl = url;
|
|
const img = new Image();
|
|
img.crossOrigin = "anonymous";
|
|
img.referrerPolicy = "no-referrer";
|
|
img.onload = () => {
|
|
try {
|
|
const c = document.createElement("canvas");
|
|
const size = 16;
|
|
c.width = size;
|
|
c.height = size;
|
|
const ctx = c.getContext("2d", { willReadFrequently: true });
|
|
ctx.drawImage(img, 0, 0, size, size);
|
|
const { data } = ctx.getImageData(0, 0, size, size);
|
|
let r = 0, g = 0, b = 0, count = 0;
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const a = data[i + 3];
|
|
if (a < 125) continue;
|
|
// skip near-black/near-white so the tint stays vivid
|
|
const lum = 0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2];
|
|
if (lum < 24 || lum > 235) continue;
|
|
r += data[i]; g += data[i + 1]; b += data[i + 2]; count++;
|
|
}
|
|
if (!count) { resetAccent(); return; }
|
|
r = Math.round(r / count); g = Math.round(g / count); b = Math.round(b / count);
|
|
// Snap the average album colour to the nearest Catppuccin accent
|
|
const near = nearestAccent(r, g, b);
|
|
const rgb = near ? `${near.r}, ${near.g}, ${near.b}` : `${r}, ${g}, ${b}`;
|
|
el.style.setProperty("--np-accent", rgb);
|
|
el.classList.add("has-accent");
|
|
// Drive the whole page's accent (nav, badges, name, link hovers…)
|
|
document.documentElement.style.setProperty("--accent-rgb", rgb);
|
|
} catch (e) {
|
|
resetAccent(); // tainted canvas / CORS — fall back to theme colour
|
|
}
|
|
};
|
|
img.onerror = resetAccent;
|
|
img.src = url;
|
|
}
|
|
|
|
function resetAccent() {
|
|
el.classList.remove("has-accent");
|
|
el.style.removeProperty("--np-accent");
|
|
// Hand the page's accent back to the active theme's pink
|
|
document.documentElement.style.removeProperty("--accent-rgb");
|
|
}
|
|
|
|
// ---- rendering ----------------------------------------------------------
|
|
function stopProgress() {
|
|
if (progressTimer) { clearInterval(progressTimer); progressTimer = null; }
|
|
}
|
|
|
|
function tickProgress(spotify) {
|
|
const start = spotify.timestamps && spotify.timestamps.start;
|
|
const end = spotify.timestamps && spotify.timestamps.end;
|
|
if (!start || !end || end <= start) {
|
|
el.classList.remove("has-progress");
|
|
return;
|
|
}
|
|
el.classList.add("has-progress");
|
|
const now = Date.now();
|
|
const elapsed = clamp(now - start, 0, end - start);
|
|
const pct = clamp((elapsed / (end - start)) * 100, 0, 100);
|
|
fillEl.style.width = pct + "%";
|
|
curEl.textContent = fmt(elapsed);
|
|
durEl.textContent = fmt(end - start);
|
|
}
|
|
|
|
function render(d) {
|
|
if (!d) return;
|
|
const status = d.discord_status || "offline";
|
|
el.dataset.status = status;
|
|
|
|
// Discord status word — always shown (coexists with the track)
|
|
statusLabelEl.textContent = STATUS_LABELS[status] || "Offline";
|
|
|
|
const spotify = d.listening_to_spotify && d.spotify ? d.spotify : null;
|
|
|
|
if (spotify) {
|
|
el.classList.add("is-live");
|
|
labelEl.textContent = "Now playing";
|
|
trackEl.textContent = spotify.song || "";
|
|
artistEl.textContent = spotify.artist || "";
|
|
|
|
if (spotify.album_art_url) {
|
|
artEl.src = spotify.album_art_url;
|
|
artEl.style.display = "";
|
|
applyAccent(spotify.album_art_url);
|
|
} else {
|
|
artEl.style.display = "none";
|
|
resetAccent();
|
|
}
|
|
|
|
el.href = spotify.track_id
|
|
? `https://open.spotify.com/track/${spotify.track_id}`
|
|
: "https://open.spotify.com/";
|
|
|
|
stopProgress();
|
|
tickProgress(spotify);
|
|
progressTimer = setInterval(() => tickProgress(spotify), 1000);
|
|
} else {
|
|
// Not listening — just the Discord status (dot + word in the head)
|
|
el.classList.remove("is-live", "has-progress");
|
|
stopProgress();
|
|
resetAccent();
|
|
trackEl.textContent = "";
|
|
artistEl.textContent = "";
|
|
artEl.style.display = "none";
|
|
el.href = "https://discord.gg/TransRights";
|
|
}
|
|
|
|
el.hidden = false;
|
|
}
|
|
|
|
// ---- Lanyard websocket --------------------------------------------------
|
|
function connect() {
|
|
ws = new WebSocket("wss://api.lanyard.rest/socket");
|
|
|
|
ws.addEventListener("message", (evt) => {
|
|
let msg;
|
|
try { msg = JSON.parse(evt.data); } catch (e) { return; }
|
|
|
|
// op 1 = Hello: start heartbeat, then subscribe
|
|
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;
|
|
}
|
|
|
|
// op 0 = Event: INIT_STATE or PRESENCE_UPDATE
|
|
if (msg.op === 0) {
|
|
const d = msg.t === "INIT_STATE"
|
|
? (msg.d && msg.d[DISCORD_USER_ID]) || msg.d
|
|
: msg.d;
|
|
latest = d;
|
|
render(d);
|
|
}
|
|
});
|
|
|
|
ws.addEventListener("open", () => { reconnectDelay = 1000; });
|
|
|
|
ws.addEventListener("close", () => {
|
|
if (heartbeat) { clearInterval(heartbeat); heartbeat = null; }
|
|
stopProgress();
|
|
// exponential backoff up to 30s
|
|
setTimeout(connect, reconnectDelay);
|
|
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
});
|
|
|
|
ws.addEventListener("error", () => { try { ws.close(); } catch (e) {} });
|
|
}
|
|
|
|
connect();
|
|
|
|
// keep the progress bar honest when returning to the tab
|
|
document.addEventListener("visibilitychange", () => {
|
|
if (!document.hidden && latest) render(latest);
|
|
});
|
|
})();
|