fix(app): Fix music block and added playing now model
This commit is contained in:
1 parent
f2e433d1e4
commit
d0985375a8
7 files changed
+171
-234
No files matched your search
+49
-22
@@ -711,33 +711,60 @@ img.card-icon { border-radius: 12px; }
|
||||
border: 1px solid rgba(94, 234, 212, .3);
|
||||
}
|
||||
|
||||
.ym-tabs {
|
||||
.ym-now {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 14px 16px;
|
||||
margin-bottom: 16px;
|
||||
border: 1px solid rgba(94, 234, 212, .25);
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(120deg, rgba(94, 234, 212, .07), rgba(255, 255, 255, .02));
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.ym-now-label {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid var(--card-border);
|
||||
}
|
||||
|
||||
.ym-tab {
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-family: var(--mono);
|
||||
font-size: .72rem;
|
||||
letter-spacing: .06em;
|
||||
padding: 5px 14px;
|
||||
border-radius: 999px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
transition: color .2s, background .2s;
|
||||
font-size: .68rem;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ym-tab:hover { color: var(--text); }
|
||||
.ym-now-body { min-width: 0; }
|
||||
|
||||
.ym-tab.active {
|
||||
background: rgba(94, 234, 212, .12);
|
||||
color: var(--accent);
|
||||
.ym-now-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a.ym-now-link:hover .ym-track-title { color: var(--accent); }
|
||||
|
||||
.ym-now-cover {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, .06);
|
||||
}
|
||||
|
||||
.ym-now-cover.ym-cover-empty { font-size: 1.5rem; }
|
||||
|
||||
.ym-now-info { display: flex; flex-direction: column; gap: 3px; min-width: 0; }
|
||||
|
||||
.ym-now-idle {
|
||||
font-family: var(--mono);
|
||||
font-size: .8rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ym-meta {
|
||||
|
||||
+61
-27
@@ -4,22 +4,20 @@
|
||||
const list = document.getElementById("ymHistory");
|
||||
const updated = document.getElementById("ymUpdated");
|
||||
const refresh = document.getElementById("ymRefresh");
|
||||
const tabs = Array.from(document.querySelectorAll(".ym-tab"));
|
||||
const nowBody = document.getElementById("ymNowBody");
|
||||
if (!list || !updated) return;
|
||||
|
||||
const SOURCES = {
|
||||
lb: "/api/listenbrainz/recent",
|
||||
ym: "/api/ym/history",
|
||||
};
|
||||
const SOURCE = "/api/listenbrainz/recent";
|
||||
const NOW_SOURCE = "/api/listenbrainz/now";
|
||||
const POLL_MS = 30000;
|
||||
const NOW_POLL_MS = 10000;
|
||||
|
||||
let current = "lb";
|
||||
let timer = null;
|
||||
let nowTimer = null;
|
||||
let busy = false;
|
||||
const state = {
|
||||
lb: { seen: new Set(), firstLoad: true },
|
||||
ym: { seen: new Set(), firstLoad: true },
|
||||
};
|
||||
let nowBusy = false;
|
||||
const seen = new Set();
|
||||
let firstLoad = true;
|
||||
|
||||
const escapeHtml = (s) =>
|
||||
String(s).replace(/[&<>"']/g, (c) => ({
|
||||
@@ -38,13 +36,12 @@
|
||||
};
|
||||
|
||||
const render = (tracks) => {
|
||||
const st = state[current];
|
||||
const fresh = [];
|
||||
const html = tracks.map((t) => {
|
||||
const key = `${t.id}:${t.album_id || ""}:${t.day || ""}`;
|
||||
if (!st.seen.has(key)) {
|
||||
st.seen.add(key);
|
||||
if (!st.firstLoad) fresh.push(key);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
if (!firstLoad) fresh.push(key);
|
||||
}
|
||||
const isNew = fresh.includes(key);
|
||||
const cover = t.cover
|
||||
@@ -69,18 +66,40 @@
|
||||
}).join("");
|
||||
|
||||
list.innerHTML = html || '<li class="ym-status">Пока нет прослушиваний</li>';
|
||||
st.firstLoad = false;
|
||||
firstLoad = false;
|
||||
};
|
||||
|
||||
const setStatus = (msg, isError) => {
|
||||
list.innerHTML = `<li class="ym-status${isError ? " ym-error" : ""}">${escapeHtml(msg)}</li>`;
|
||||
};
|
||||
|
||||
const renderNow = (track) => {
|
||||
if (!nowBody) return;
|
||||
if (!track) {
|
||||
nowBody.innerHTML = '<span class="ym-now-idle">ничего не играет</span>';
|
||||
return;
|
||||
}
|
||||
const cover = track.cover
|
||||
? `<img class="ym-now-cover" src="${escapeHtml(track.cover)}" alt="" loading="lazy" width="72" height="72">`
|
||||
: `<span class="ym-now-cover ym-cover-empty" aria-hidden="true">♪</span>`;
|
||||
const artists = (track.artists || []).map(escapeHtml).join(", ");
|
||||
const context = track.context ? ` <span class="ym-context">· ${escapeHtml(track.context)}</span>` : "";
|
||||
const inner = `
|
||||
${cover}
|
||||
<span class="ym-now-info">
|
||||
<span class="ym-track-title">${escapeHtml(track.title)}</span>
|
||||
<span class="ym-artists">${artists || "—"}${context}</span>
|
||||
</span>`;
|
||||
nowBody.innerHTML = track.url
|
||||
? `<a class="ym-now-link" href="${escapeHtml(track.url)}" target="_blank" rel="noopener">${inner}</a>`
|
||||
: `<div class="ym-now-link">${inner}</div>`;
|
||||
};
|
||||
|
||||
const tick = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(SOURCES[current], { headers: { Accept: "application/json" } });
|
||||
const res = await fetch(SOURCE, { headers: { Accept: "application/json" } });
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
setStatus(data.error || `Ошибка ${res.status}`, true);
|
||||
@@ -98,18 +117,32 @@
|
||||
}
|
||||
};
|
||||
|
||||
const poke = () => { if (timer) clearTimeout(timer); tick(); };
|
||||
const tickNow = async () => {
|
||||
if (nowBusy) return;
|
||||
nowBusy = true;
|
||||
try {
|
||||
const res = await fetch(NOW_SOURCE, { headers: { Accept: "application/json" } });
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
if (nowBody) nowBody.innerHTML = `<span class="ym-now-idle">${escapeHtml(data.error || `Ошибка ${res.status}`)}</span>`;
|
||||
return;
|
||||
}
|
||||
renderNow(data.track);
|
||||
} catch (e) {
|
||||
if (nowBody) nowBody.innerHTML = '<span class="ym-now-idle">нет связи</span>';
|
||||
} finally {
|
||||
nowBusy = false;
|
||||
if (nowTimer) clearTimeout(nowTimer);
|
||||
nowTimer = setTimeout(tickNow, NOW_POLL_MS);
|
||||
}
|
||||
};
|
||||
|
||||
tabs.forEach((tab) => {
|
||||
tab.addEventListener("click", () => {
|
||||
const source = tab.dataset.source;
|
||||
if (!source || source === current) return;
|
||||
current = source;
|
||||
tabs.forEach((t) => t.classList.toggle("active", t === tab));
|
||||
updated.textContent = "подключение…";
|
||||
poke();
|
||||
});
|
||||
});
|
||||
const poke = () => {
|
||||
if (timer) clearTimeout(timer);
|
||||
if (nowTimer) clearTimeout(nowTimer);
|
||||
tick();
|
||||
tickNow();
|
||||
};
|
||||
|
||||
if (refresh) refresh.addEventListener("click", poke);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
@@ -117,4 +150,5 @@
|
||||
});
|
||||
|
||||
tick();
|
||||
tickNow();
|
||||
})();
|
||||
Reference in new issue
Block a user