Web: live Stem Now status that tracks every queued song (albums too), right-click menus with Stem Now, Songs and Genres pages, a Stems Only filter in place of the sidebar section, Random sort, several pooled Plex servers and libraries, themes plus custom CSS, a reorganised settings page that loads without a refresh, saved mixes, karaoke and synced lyrics, stem playlists and offline downloads. iOS: Random in every sort menu, a Stems Only toggle (sort menu and Settings), and several music libraries pooled from the signed-in server. Settings now persists karaoke and speed across launches. stemd writes a status file that stemserve serves at /api/queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
50 lines
4.1 KiB
JavaScript
50 lines
4.1 KiB
JavaScript
// Small helpers shared by the web player's modules.
|
|
|
|
export const $ = (s, r = document) => r.querySelector(s);
|
|
export const $$ = (s, r = document) => [...r.querySelectorAll(s)];
|
|
export const esc = s => String(s ?? "").replace(/[&<>"']/g, c => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]);
|
|
export const fmtTime = s => { s = Math.max(0, Math.floor(s || 0)); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; };
|
|
export const fmtBytes = n => n >= 1e9 ? (n / 1e9).toFixed(1) + " GB" : (n / 1e6).toFixed(n >= 1e8 ? 0 : 1) + " MB";
|
|
export const plural = (n, w) => `${n.toLocaleString()} ${w}${n === 1 ? "" : "s"}`;
|
|
export const shuffled = a => { a = a.slice(); for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; };
|
|
export const clone = o => JSON.parse(JSON.stringify(o ?? null));
|
|
// crypto.randomUUID only exists on HTTPS pages, and the player is usually opened over plain HTTP.
|
|
export const uuid = () => crypto.randomUUID ? crypto.randomUUID() : Date.now().toString(36) + Math.random().toString(36).slice(2);
|
|
|
|
export function load(k) { try { return JSON.parse(localStorage.getItem(k)); } catch { return null; } }
|
|
export function store(k, v) { try { localStorage.setItem(k, JSON.stringify(v)); } catch {} }
|
|
|
|
// Lets the player tell the UI what changed without importing it.
|
|
const bus = {};
|
|
export const on = (ev, fn) => { (bus[ev] ||= []).push(fn); };
|
|
export const emit = (ev, ...args) => { for (const fn of bus[ev] || []) fn(...args); };
|
|
|
|
export function toast(msg, ms = 2400) {
|
|
let t = $("#toast");
|
|
if (!t) {
|
|
t = document.createElement("div");
|
|
t.id = "toast";
|
|
document.body.append(t);
|
|
}
|
|
t.textContent = msg;
|
|
t.classList.add("show");
|
|
clearTimeout(t._h);
|
|
t._h = setTimeout(() => t.classList.remove("show"), ms);
|
|
}
|
|
|
|
export const ICON = {
|
|
play: '<svg viewBox="0 0 24 24"><path d="M7 4.5v15a1 1 0 0 0 1.5.86l12.5-7.5a1 1 0 0 0 0-1.72L8.5 3.64A1 1 0 0 0 7 4.5z"/></svg>',
|
|
pause: '<svg viewBox="0 0 24 24"><rect x="5.5" y="4" width="4.5" height="16" rx="1.2"/><rect x="14" y="4" width="4.5" height="16" rx="1.2"/></svg>',
|
|
prev: '<svg viewBox="0 0 24 24"><rect x="4" y="5" width="2.5" height="14" rx="1"/><path d="M20 5.7v12.6a1 1 0 0 1-1.55.83L9 12.83a1 1 0 0 1 0-1.66l9.45-6.3A1 1 0 0 1 20 5.7z"/></svg>',
|
|
next: '<svg viewBox="0 0 24 24"><rect x="17.5" y="5" width="2.5" height="14" rx="1"/><path d="M4 5.7v12.6a1 1 0 0 0 1.55.83L15 12.83a1 1 0 0 0 0-1.66l-9.45-6.3A1 1 0 0 0 4 5.7z"/></svg>',
|
|
shuffle: '<svg viewBox="0 0 24 24"><path d="M17 3l4 4-4 4V8h-2.2c-1 0-1.9.5-2.5 1.3L7.6 16c-.9 1.3-2.4 2-4 2H2v-2h1.6c.9 0 1.8-.5 2.3-1.2l4.7-6.7C11.5 6.8 13 6 14.8 6H17V3zM2 6h1.6c1.6 0 3.1.8 4 2l.9 1.3-1.2 1.7-1.4-1.9A2.8 2.8 0 0 0 3.6 8H2V6zm12.3 9.7c.6.8 1.5 1.3 2.5 1.3H17v-3l4 4-4 4v-3h-.2c-1.7 0-3.3-.8-4.2-2.2l-.6-.8 1.2-1.7.1.4z"/></svg>',
|
|
repeat: '<svg viewBox="0 0 24 24"><path d="M17 1l4 4-4 4V6H7a2 2 0 0 0-2 2v3H3V8a4 4 0 0 1 4-4h10V1zM7 23l-4-4 4-4v3h10a2 2 0 0 0 2-2v-3h2v3a4 4 0 0 1-4 4H7v3z"/></svg>',
|
|
repeat1: '<svg viewBox="0 0 24 24"><path d="M17 1l4 4-4 4V6H7a2 2 0 0 0-2 2v3H3V8a4 4 0 0 1 4-4h10V1zM7 23l-4-4 4-4v3h10a2 2 0 0 0 2-2v-3h2v3a4 4 0 0 1-4 4H7v3z"/><path d="M11.2 9.6l1.8-.9h1.2v6.6h-1.6v-4.9l-1.4.6z"/></svg>',
|
|
mixer: '<svg viewBox="0 0 24 24"><path d="M6 3h2v6.2a3 3 0 0 1 0 5.6V21H6v-6.2a3 3 0 0 1 0-5.6V3zm10 0h2v2.2a3 3 0 0 1 0 5.6V21h-2V10.8a3 3 0 0 1 0-5.6V3zM7 11a1 1 0 1 0 0 2 1 1 0 0 0 0-2zm10-4a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>',
|
|
queue: '<svg viewBox="0 0 24 24"><path d="M3 5h14v2H3zm0 6h14v2H3zm0 6h9v2H3zm13-1.5v-1.5l6 3.5-6 3.5V17z"/></svg>',
|
|
lyrics: '<svg viewBox="0 0 24 24"><path d="M12 2a4 4 0 0 1 4 4v5a4 4 0 0 1-8 0V6a4 4 0 0 1 4-4zm-7 9h2a5 5 0 0 0 10 0h2a7 7 0 0 1-6 6.93V21h3v2H8v-2h3v-3.07A7 7 0 0 1 5 11z"/></svg>',
|
|
more: '<svg viewBox="0 0 24 24"><circle cx="5" cy="12" r="2"/><circle cx="12" cy="12" r="2"/><circle cx="19" cy="12" r="2"/></svg>',
|
|
download: '<svg viewBox="0 0 24 24"><path d="M11 3h2v10.2l3.6-3.6 1.4 1.4-6 6-6-6 1.4-1.4 3.6 3.6V3zM4 19h16v2H4z"/></svg>',
|
|
busy: '<svg viewBox="0 0 24 24"><path d="M12 3a9 9 0 1 0 9 9h-2.2A6.8 6.8 0 1 1 12 5.2V3z"/></svg>',
|
|
};
|