feat(app): Add yandex music status
This commit is contained in:
1 parent
76032d9869
commit
a87da8df5b
7 files changed
+433
-1
No files matched your search
@@ -673,6 +673,176 @@ img.card-icon { border-radius: 12px; }
|
||||
color: #b6beff;
|
||||
}
|
||||
|
||||
/* ---------- трекер Яндекс Музыки ---------- */
|
||||
.ym-tracker {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.ym-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.ym-title { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
||||
|
||||
.ym-title h2 {
|
||||
font-size: clamp(1.1rem, 3vw, 1.4rem);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.ym-live {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-family: var(--mono);
|
||||
font-size: .68rem;
|
||||
letter-spacing: .14em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: rgba(94, 234, 212, .08);
|
||||
border: 1px solid rgba(94, 234, 212, .3);
|
||||
}
|
||||
|
||||
.ym-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: var(--mono);
|
||||
font-size: .74rem;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.ym-refresh {
|
||||
font: inherit;
|
||||
color: var(--muted);
|
||||
background: rgba(255, 255, 255, .04);
|
||||
border: 1px solid rgba(255, 255, 255, .1);
|
||||
border-radius: 8px;
|
||||
padding: 3px 9px;
|
||||
cursor: pointer;
|
||||
transition: color .2s, border-color .2s;
|
||||
}
|
||||
|
||||
.ym-refresh:hover { color: var(--accent); border-color: rgba(94, 234, 212, .4); }
|
||||
|
||||
.ym-list {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
counter-reset: ym;
|
||||
}
|
||||
|
||||
.ym-status {
|
||||
font-family: var(--mono);
|
||||
font-size: .84rem;
|
||||
color: var(--muted);
|
||||
padding: 18px;
|
||||
text-align: center;
|
||||
background: var(--card);
|
||||
border: 1px dashed var(--card-border);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.ym-status.ym-error { color: #fca5a5; border-color: rgba(248, 113, 113, .35); }
|
||||
|
||||
.ym-track { counter-increment: ym; }
|
||||
|
||||
.ym-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 10px 14px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--card-border);
|
||||
border-radius: 14px;
|
||||
transition: transform .2s ease, border-color .2s ease, background .2s ease;
|
||||
}
|
||||
|
||||
.ym-link::before {
|
||||
content: counter(ym, decimal-leading-zero);
|
||||
font-family: var(--mono);
|
||||
font-size: .74rem;
|
||||
color: var(--muted);
|
||||
min-width: 22px;
|
||||
}
|
||||
|
||||
.ym-link:hover {
|
||||
transform: translateX(4px);
|
||||
border-color: rgba(94, 234, 212, .4);
|
||||
background: rgba(94, 234, 212, .06);
|
||||
}
|
||||
|
||||
.ym-cover {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: rgba(255, 255, 255, .06);
|
||||
}
|
||||
|
||||
.ym-cover-empty {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--accent);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.ym-info { display: flex; flex-direction: column; min-width: 0; flex: 1; }
|
||||
|
||||
.ym-track-title {
|
||||
font-weight: 600;
|
||||
font-size: .95rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ym-artists {
|
||||
font-size: .82rem;
|
||||
color: var(--muted);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.ym-context { font-family: var(--mono); font-size: .74rem; }
|
||||
|
||||
.ym-day {
|
||||
font-family: var(--mono);
|
||||
font-size: .72rem;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
color: var(--muted);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ym-track.ym-new .ym-link {
|
||||
animation: ym-pop .6s cubic-bezier(.22, 1, .36, 1);
|
||||
border-color: rgba(94, 234, 212, .55);
|
||||
box-shadow: 0 0 0 3px rgba(94, 234, 212, .12);
|
||||
}
|
||||
|
||||
@keyframes ym-pop {
|
||||
from { opacity: 0; transform: translateY(-8px) scale(.98); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.ym-day { display: none; }
|
||||
.ym-link { padding: 9px 11px; gap: 10px; }
|
||||
}
|
||||
|
||||
/* ---------- кнопка друга ---------- */
|
||||
.friend-cta {
|
||||
display: flex;
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const list = document.getElementById("ymHistory");
|
||||
const updated = document.getElementById("ymUpdated");
|
||||
const refresh = document.getElementById("ymRefresh");
|
||||
if (!list || !updated) return;
|
||||
|
||||
const POLL_MS = 30000;
|
||||
const seen = new Set();
|
||||
let firstLoad = true;
|
||||
let timer = null;
|
||||
let busy = false;
|
||||
|
||||
const escapeHtml = (s) =>
|
||||
String(s).replace(/[&<>"']/g, (c) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||||
})[c]);
|
||||
|
||||
const dayLabel = (day) => {
|
||||
if (!day) return "";
|
||||
const today = new Date().toISOString().slice(0, 10);
|
||||
const yest = new Date(Date.now() - 86400000).toISOString().slice(0, 10);
|
||||
if (day === today) return "сегодня";
|
||||
if (day === yest) return "вчера";
|
||||
const [y, m, d] = day.split("-");
|
||||
return `${d}.${m}.${y}`;
|
||||
};
|
||||
|
||||
const render = (tracks) => {
|
||||
const fresh = [];
|
||||
const html = tracks.map((t) => {
|
||||
const key = `${t.id}:${t.album_id || ""}:${t.day}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
if (!firstLoad) fresh.push(key);
|
||||
}
|
||||
const isNew = fresh.includes(key);
|
||||
const cover = t.cover
|
||||
? `<img class="ym-cover" src="${escapeHtml(t.cover)}" alt="" loading="lazy" width="48" height="48">`
|
||||
: `<span class="ym-cover ym-cover-empty" aria-hidden="true">♪</span>`;
|
||||
const artists = (t.artists || []).map(escapeHtml).join(", ");
|
||||
const context = t.context ? ` <span class="ym-context">· ${escapeHtml(t.context)}</span>` : "";
|
||||
return `<li class="ym-track${isNew ? " ym-new" : ""}">
|
||||
<a class="ym-link" href="${escapeHtml(t.url)}" target="_blank" rel="noopener">
|
||||
${cover}
|
||||
<span class="ym-info">
|
||||
<span class="ym-track-title">${escapeHtml(t.title)}</span>
|
||||
<span class="ym-artists">${artists || "—"}${context}</span>
|
||||
</span>
|
||||
<span class="ym-day">${dayLabel(t.day)}</span>
|
||||
</a>
|
||||
</li>`;
|
||||
}).join("");
|
||||
|
||||
list.innerHTML = html || '<li class="ym-status">Пока нет прослушиваний за последние дни</li>';
|
||||
firstLoad = false;
|
||||
};
|
||||
|
||||
const setStatus = (msg, isError) => {
|
||||
list.innerHTML = `<li class="ym-status${isError ? " ym-error" : ""}">${escapeHtml(msg)}</li>`;
|
||||
};
|
||||
|
||||
const tick = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch("/api/ym/history", { headers: { Accept: "application/json" } });
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
setStatus(data.error || `Ошибка ${res.status}`, true);
|
||||
return;
|
||||
}
|
||||
render(data.tracks || []);
|
||||
const t = new Date(data.updated_at * 1000);
|
||||
updated.textContent = `обновлено ${t.toLocaleTimeString("ru-RU")}${data.stale ? " (кэш)" : ""}`;
|
||||
} catch (e) {
|
||||
setStatus("Нет связи с сервером", true);
|
||||
} finally {
|
||||
busy = false;
|
||||
if (timer) clearTimeout(timer);
|
||||
timer = setTimeout(tick, POLL_MS);
|
||||
}
|
||||
};
|
||||
|
||||
const poke = () => { if (timer) clearTimeout(timer); tick(); };
|
||||
|
||||
if (refresh) refresh.addEventListener("click", poke);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (!document.hidden) poke();
|
||||
});
|
||||
|
||||
tick();
|
||||
})();
|
||||
Reference in new issue
Block a user