feat(steam): Added steam activity
This commit is contained in:
1 parent
1ea9e22d8f
commit
e528b99743
5 files changed
+417
-16
No files matched your search
@@ -0,0 +1,138 @@
|
||||
(() => {
|
||||
"use strict";
|
||||
|
||||
const recentList = document.getElementById("steamRecent");
|
||||
const topList = document.getElementById("steamTop");
|
||||
const updated = document.getElementById("steamUpdated");
|
||||
const refresh = document.getElementById("steamRefresh");
|
||||
const countEl = document.getElementById("steamCount");
|
||||
if (!recentList || !topList || !updated) return;
|
||||
|
||||
const SOURCE = "/api/steam/activity";
|
||||
const POLL_MS = 60000;
|
||||
|
||||
let timer = null;
|
||||
let busy = false;
|
||||
|
||||
const escapeHtml = (s) =>
|
||||
String(s).replace(/[&<>"']/g, (c) => ({
|
||||
"&": "&", "<": "<", ">": ">", '"': """, "'": "'",
|
||||
})[c]);
|
||||
|
||||
const IMG_FALLBACK = "data:image/svg+xml," + encodeURIComponent(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'>" +
|
||||
"<rect width='48' height='48' rx='8' fill='#0c0e16'/>" +
|
||||
"<text x='24' y='31' font-size='20' text-anchor='middle'>🎮</text></svg>"
|
||||
);
|
||||
|
||||
const fmtHours = (h) => {
|
||||
if (h == null) return "";
|
||||
if (h >= 168) return `${(h / 24).toFixed(1)} д`;
|
||||
if (h >= 1) return `${h.toFixed(1)} ч`;
|
||||
return `${Math.round(h * 60)} мин`;
|
||||
};
|
||||
|
||||
const fmtDate = (ts) => {
|
||||
if (!ts) return "";
|
||||
const d = new Date(ts * 1000);
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
if (d >= today) return "сегодня";
|
||||
if (d >= new Date(today.getTime() - 86400000)) return "вчера";
|
||||
return d.toLocaleDateString("ru-RU", { day: "2-digit", month: "2-digit", year: "numeric" });
|
||||
};
|
||||
|
||||
const bindCovers = (root) => {
|
||||
root.querySelectorAll("img.st-cover").forEach((img) => {
|
||||
img.addEventListener("error", () => { img.src = IMG_FALLBACK; }, { once: true });
|
||||
});
|
||||
};
|
||||
|
||||
const rowHtml = (g, right, sub) => `<li class="ym-track">
|
||||
<a class="ym-link" href="${escapeHtml(g.url)}" target="_blank" rel="noopener">
|
||||
<img class="ym-cover st-cover" src="${escapeHtml(g.header)}" alt="" loading="lazy" width="48" height="48">
|
||||
<span class="ym-info">
|
||||
<span class="ym-track-title">${escapeHtml(g.name)}</span>
|
||||
<span class="ym-artists">${sub}</span>
|
||||
</span>
|
||||
<span class="ym-day">${right}</span>
|
||||
</a>
|
||||
</li>`;
|
||||
|
||||
const emptyStatus = (closed) => ({
|
||||
html: closed
|
||||
? "Детали игр скрыты в настройках приватности Steam"
|
||||
: "Пока нет данных",
|
||||
cls: closed ? "ym-status ym-error" : "ym-status",
|
||||
});
|
||||
|
||||
const renderGames = (data) => {
|
||||
const closed = Boolean(data.profile && data.profile.public === false);
|
||||
|
||||
const recentHtml = (data.recent || []).map((g) => {
|
||||
const parts = [];
|
||||
if (g.hours_total) parts.push(`всего ${fmtHours(g.hours_total)}`);
|
||||
const last = fmtDate(g.last_played);
|
||||
if (last) parts.push(`играл ${last}`);
|
||||
const right = g.hours_2weeks != null ? `${fmtHours(g.hours_2weeks)} / 2 нед` : "";
|
||||
return rowHtml(g, right, parts.join(" · ") || "—");
|
||||
}).join("");
|
||||
|
||||
const topHtml = (data.top || []).map((g) => {
|
||||
const last = fmtDate(g.last_played);
|
||||
const sub = last ? `играл ${last}` : "—";
|
||||
return rowHtml(g, fmtHours(g.hours_total), sub);
|
||||
}).join("");
|
||||
|
||||
const recentEmpty = emptyStatus(closed);
|
||||
const topEmpty = emptyStatus(closed);
|
||||
recentList.innerHTML = recentHtml || `<li class="${recentEmpty.cls}">${recentEmpty.html}</li>`;
|
||||
topList.innerHTML = topHtml || `<li class="${topEmpty.cls}">${topEmpty.html}</li>`;
|
||||
|
||||
bindCovers(recentList);
|
||||
bindCovers(topList);
|
||||
|
||||
if (countEl) countEl.textContent = data.game_count ? `всего игр: ${data.game_count}` : "";
|
||||
};
|
||||
|
||||
const showError = (msg) => {
|
||||
const html = `<li class="ym-status ym-error">${escapeHtml(msg)}</li>`;
|
||||
recentList.innerHTML = html;
|
||||
topList.innerHTML = html;
|
||||
updated.textContent = "ошибка";
|
||||
};
|
||||
|
||||
const tick = async () => {
|
||||
if (busy) return;
|
||||
busy = true;
|
||||
try {
|
||||
const res = await fetch(SOURCE, { headers: { Accept: "application/json" } });
|
||||
const data = await res.json();
|
||||
if (!res.ok || !data.ok) {
|
||||
showError(data.error || `Ошибка ${res.status}`);
|
||||
return;
|
||||
}
|
||||
renderGames(data);
|
||||
const t = new Date(data.updated_at * 1000);
|
||||
updated.textContent = `обновлено ${t.toLocaleTimeString("ru-RU")}${data.stale ? " (кэш)" : ""}`;
|
||||
} catch (e) {
|
||||
showError("Нет связи с сервером");
|
||||
} 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