339 lines
10 KiB
Plaintext
339 lines
10 KiB
Plaintext
(() => {
|
||
// =========================
|
||
// 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();
|
||
})();
|