2026-07-14 11:14:13 +03:00

339 lines
10 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
// =========================
// Utils / status
// =========================
const elGlobalStatus = document.getElementById("globalStatus");
const setStatus = (txt) => {
if (!elGlobalStatus) return;
elGlobalStatus.textContent = txt || "";
if (txt) setTimeout(() => (elGlobalStatus.textContent = ""), 1200);
};
const debounceMap = new Map();
const debounce = (key, fn, ms) => {
if (debounceMap.has(key)) clearTimeout(debounceMap.get(key));
debounceMap.set(key, setTimeout(fn, ms));
};
// Base URL from current page
const HTTP_BASE = `${location.protocol}//${location.host}`;
const WS_URL = (() => {
const proto = location.protocol === "https:" ? "wss" : "ws";
return `${proto}://${location.host}/ws`;
})();
// Helpful logs (можешь потом удалить)
console.log("Page:", location.href);
console.log("HTTP_BASE:", HTTP_BASE);
console.log("WS_URL:", WS_URL);
// =========================
// HTTP helpers
// =========================
async function postJson(path, payload) {
const r = await fetch(`${HTTP_BASE}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload || {}),
});
return r;
}
async function sendSetting(id, value) {
try {
const r = await postJson("/api/setting", { id, value });
if (!r.ok) {
setStatus("Ошибка применения");
return;
}
setStatus("Сохранено");
} catch (e) {
setStatus("Сервер недоступен");
}
}
async function sendCoins(amount) {
const coinsStatus = document.getElementById("coinsStatus");
try {
if (coinsStatus) coinsStatus.textContent = "";
const r = await postJson("/api/coins", { amount });
if (!r.ok) {
if (coinsStatus) coinsStatus.textContent = "Ошибка запроса";
return;
}
if (coinsStatus) {
coinsStatus.textContent = "Ок";
setTimeout(() => (coinsStatus.textContent = ""), 1200);
}
} catch (e) {
if (coinsStatus) coinsStatus.textContent = "Сервер недоступен";
}
}
async function sendAction(name, value) {
// value — опционально (для новой схемы: action + payload)
try {
const r = await postJson("/api/action", value !== undefined ? { name, value } : { name });
const t = await r.text();
if (!r.ok) {
setStatus("Ошибка действия");
try {
const j = JSON.parse(t);
if (j && j.message) setStatus(j.message);
} catch (_) {}
return false;
}
setStatus("Ок");
return true;
} catch (e) {
setStatus("Сервер недоступен");
return false;
}
}
// =========================
// Bind UI events (existing page)
// =========================
// Settings controls by data-setting-id
document.querySelectorAll("[data-setting-id]").forEach((el) => {
const id = el.getAttribute("data-setting-id");
if (!id) return;
if (el.type === "checkbox") {
el.addEventListener("change", () => sendSetting(id, el.checked ? "on" : "off"));
return;
}
if (el.type === "range") {
const valueSpanId = el.getAttribute("data-value-span");
const valueSpan = valueSpanId ? document.getElementById(valueSpanId) : null;
const apply = () => {
if (valueSpan) valueSpan.textContent = el.value;
debounce("rng_" + id, () => sendSetting(id, el.value), 120);
};
el.addEventListener("input", apply);
el.addEventListener("change", apply);
return;
}
if (el.type === "radio") {
el.addEventListener("change", () => {
if (el.checked) sendSetting(id, el.value);
});
return;
}
// number/text
el.addEventListener("change", () => sendSetting(id, el.value));
});
// Coins: input + button
const coinsAmount = document.getElementById("coinsAmount");
const addCoinsBtn = document.getElementById("addCoinsBtn");
if (addCoinsBtn) {
addCoinsBtn.addEventListener("click", async () => {
const v = parseInt((coinsAmount && coinsAmount.value) || "0", 10);
if (!Number.isFinite(v) || v <= 0) {
const coinsStatus = document.getElementById("coinsStatus");
if (coinsStatus) coinsStatus.textContent = "Введи число > 0";
return;
}
await sendCoins(v);
});
}
// Actions (buttons)
document.querySelectorAll("[data-action-name]").forEach((btn) => {
btn.addEventListener("click", async () => {
const name = btn.getAttribute("data-action-name") || "";
if (!name) return;
// Если кнопка "Сохранить" в карточках цен/валют — берём значение инпута рядом
// (не влияет на обычные action-btn)
let value;
const card = btn.closest(".price-card, .currency-card");
if (card) {
const input = card.querySelector("input");
if (input) value = input.value;
}
try {
btn.disabled = true;
await sendAction(name, value);
} finally {
setTimeout(() => (btn.disabled = false), 200);
}
});
});
// =========================
// WebSocket (preparing for new scheme)
// =========================
let ws = null;
let wsTimer = null;
let wsBackoffMs = 500;
function wsConnect() {
if (ws) {
try { ws.close(); } catch (_) {}
ws = null;
}
if (wsTimer) {
clearTimeout(wsTimer);
wsTimer = null;
}
console.log("WS connecting:", WS_URL);
try {
ws = new WebSocket(WS_URL);
} catch (e) {
console.log("WS create failed:", e);
scheduleReconnect();
return;
}
ws.addEventListener("open", () => {
console.log("WS open");
wsBackoffMs = 500;
// На будущее: можно отправлять hello/subscribe
// ws.send(JSON.stringify({ op: "hello", page: "settings" }));
});
ws.addEventListener("message", (ev) => {
// Сервер дальше будет слать JSON snapshot/patch
try {
const msg = JSON.parse(ev.data);
handleWsMessage(msg);
} catch (e) {
console.log("WS message not json:", ev.data);
}
});
ws.addEventListener("close", (e) => {
console.log("WS close", e.code, e.reason);
scheduleReconnect();
});
ws.addEventListener("error", (e) => {
console.log("WS error", e);
// Обычно после error будет close, но подстрахуемся
scheduleReconnect();
});
}
function scheduleReconnect() {
if (wsTimer) return;
const delay = Math.min(wsBackoffMs, 5000);
wsBackoffMs = Math.min(wsBackoffMs * 1.6, 5000);
wsTimer = setTimeout(() => {
wsTimer = null;
wsConnect();
}, delay);
}
// ===== WS message protocol (заготовка) =====
// Ожидаем:
// { op:"snapshot", data:{ counters:{}, hints:{}, characterPrizes:[] } }
// { op:"patch", data:{ counters:{...} } }
function handleWsMessage(msg) {
if (!msg || !msg.op) return;
if (msg.op === "snapshot" || msg.op === "patch") {
const data = msg.data || {};
if (data.counters) applyCounters(data.counters);
if (data.hints) applyHints(data.hints);
if (data.characterPrizes) applyCharacterPrizes(data.characterPrizes);
if (data.passwordStatus) applyPasswordStatus(data.passwordStatus);
return;
}
if (msg.op === "toast") {
if (msg.text) setStatus(String(msg.text));
return;
}
}
// =========================
// Apply data to DOM (no visual changes)
// =========================
// counters: { key: value }
// Важно: сейчас твои счетчики — это .form-group.counter где внутри label + .status
// Мы ищем по тексту label (как fallback), но лучше позже добавить data-counter-key в html.
function applyCounters(counters) {
// 1) special ids if exist
if (counters.impulsesResetCurrent !== undefined) {
const el = document.getElementById("impulsesResetCurrent");
if (el) el.textContent = String(counters.impulsesResetCurrent);
}
// 2) generic: match label text -> value
document.querySelectorAll(".form-group.counter").forEach((fg) => {
const label = fg.querySelector("label");
const st = fg.querySelector(".status");
if (!label || !st) return;
const key = label.textContent.trim();
if (Object.prototype.hasOwnProperty.call(counters, key)) {
st.textContent = String(counters[key]);
}
});
}
function applyHints(hints) {
// пример: { impulsesResetHint: "..." }
if (hints.impulsesResetHint !== undefined) {
const el = document.getElementById("impulsesResetHint");
if (el) el.textContent = String(hints.impulsesResetHint);
}
}
// characterPrizes: [{ character:"...", prize:"..." }, ...] или массив строк
function applyCharacterPrizes(list) {
const root = document.getElementById("characterPrizes");
const empty = document.getElementById("characterPrizesEmpty");
if (!root) return;
root.innerHTML = "";
if (!list || !list.length) {
if (empty) empty.style.display = "block";
return;
}
if (empty) empty.style.display = "none";
// вывод без смены стиля: просто строки в .status
list.forEach((row) => {
const line = document.createElement("div");
line.className = "status";
if (typeof row === "string") {
line.textContent = row;
} else if (row && typeof row === "object") {
const c = row.character ?? row.name ?? "";
const p = row.prize ?? row.value ?? "";
line.textContent = `${c}: ${p}`.trim();
} else {
line.textContent = String(row);
}
root.appendChild(line);
});
}
function applyPasswordStatus(statusText) {
const el = document.getElementById("settingsPasswordStatus");
if (el && statusText !== undefined) el.textContent = String(statusText);
}
// Start WS
wsConnect();
})();