feat(steam): Added steam activity
Build, Test and Push Flask App / docker-build-and-push (push) Successful in 1m16s
Build, Test and Push Flask App / deploy (push) Successful in 1m29s

This commit is contained in:
Black_Raven committed 2026-10-08 21:01:10 +03:00
1 parent 1ea9e22d8f
commit 59558202fb
5 files changed
+448 -16

No files matched your search

+56
View File
@@ -1041,6 +1041,62 @@ a.ym-now-link:hover .ym-track-title { color: var(--accent); }
.ym-link { padding: 9px 11px; gap: 10px; }
}
/* ---------- активность: музыка слева, Steam справа ---------- */
.activity-grid {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
gap: 26px;
align-items: start;
}
.activity-grid .ym-tracker {
max-width: none;
margin: 0;
padding: 0;
}
.steam-panel { min-width: 0; }
.st-sub {
display: flex;
align-items: center;
gap: 12px;
margin: 18px 0 10px;
}
.st-sub h3 {
font-family: var(--mono);
font-size: .74rem;
font-weight: 600;
letter-spacing: .12em;
text-transform: uppercase;
color: var(--muted);
white-space: nowrap;
}
.st-sub::after {
content: "";
flex: 1;
height: 1px;
background: rgba(255, 255, 255, .07);
}
.st-foot {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: 8px 14px;
margin-top: 16px;
font-family: var(--mono);
font-size: .76rem;
color: var(--muted);
}
@media (max-width: 900px) {
.activity-grid { grid-template-columns: 1fr; }
}
/* ---------- кнопка друга ---------- */
.friend-cta {
display: flex;
+164
View File
@@ -0,0 +1,164 @@
(() => {
"use strict";
const recentList = document.getElementById("steamRecent");
const topList = document.getElementById("steamTop");
const updated = document.getElementById("steamUpdated");
const refresh = document.getElementById("steamRefresh");
const nowLabel = document.getElementById("steamNowLabel");
const nowBody = document.getElementById("steamNowBody");
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) => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
})[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 renderNow = (data) => {
if (!nowBody) return;
const now = data.now;
if (now) {
if (nowLabel) nowLabel.innerHTML = '<span class="pulse"></span>сейчас в игре';
const name = escapeHtml(now.name || "Steam");
nowBody.innerHTML = `<a class="ym-now-link" href="${escapeHtml(now.url)}" target="_blank" rel="noopener">
<img class="ym-now-cover st-cover" src="${escapeHtml(now.header)}" alt="" loading="lazy" width="72" height="72">
<span class="ym-now-info">
<span class="ym-track-title">${name}</span>
<span class="ym-artists">открыть в Steam</span>
</span>
</a>`;
bindCovers(nowBody);
} else {
if (nowLabel) nowLabel.innerHTML = '<span class="pulse"></span>статус';
const state = data.state || "";
const text = !state || state === "не в сети" ? "не в сети" : `не в игре · ${state}`;
nowBody.innerHTML = `<span class="ym-now-idle">${escapeHtml(text)}</span>`;
}
};
const showError = (msg) => {
const html = `<li class="ym-status ym-error">${escapeHtml(msg)}</li>`;
recentList.innerHTML = html;
topList.innerHTML = html;
if (nowBody) nowBody.innerHTML = `<span class="ym-now-idle">${escapeHtml(msg)}</span>`;
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);
renderNow(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();
})();