import { api, airQuality, configureTheme, isDemo, modeInfo, numberOrDash, panelUrl, showToast } from "./api.js"; const requestedLayout = new URLSearchParams(window.location.search).get("layout"); const layoutMode = ["horizontal", "vertical"].includes(requestedLayout) ? requestedLayout : null; const wideLayout = window.matchMedia("(min-width: 800px)"); function applyLayout() { document.documentElement.dataset.widgetLayout = layoutMode || (wideLayout.matches ? "horizontal" : "vertical"); } applyLayout(); wideLayout.addEventListener("change", () => !layoutMode && applyLayout()); const $ = selector => document.querySelector(selector); const themeMode = configureTheme($("#theme-button")); let state = null; let busy = false; if (isDemo) $("#demo-note").hidden = false; function setText(selector, value) { $(selector).textContent = value; } function setPill(element, label, tone) { element.textContent = label; element.className = `pill ${tone}`; } function toneClass(tone) { return ["good", "warn", "bad"].includes(tone) ? tone : "muted"; } function updateSpeedVisual(value) { const speed = Math.max(1, Math.min(6, Number(value) || 1)); setText("#speed", speed); $("#speed").classList.remove("skeleton"); document.querySelectorAll("[data-speed]").forEach(button => { const segment = Number(button.dataset.speed); button.classList.toggle("active", segment <= speed); button.setAttribute("aria-pressed", String(segment === speed)); }); } function updateTemperatureVisual(value) { const temperature = Math.max(5, Math.min(30, Number(value) || 5)); setText("#target-temp", temperature); $("#target-temp").classList.remove("skeleton"); $("#temperature-range").value = temperature; } function drawToggle(selector, active) { const button = $(selector); button.setAttribute("aria-pressed", String(Boolean(active))); button.textContent = active ? "ON" : "OFF"; } function render(status) { state = status; const tion = status.tion || {}; const sensor = status.qingping || {}; const schedule = status.schedule || {}; const mode = modeInfo(status); const quality = airQuality(sensor.co2); const headerMode = schedule.override_active && schedule.override_until_time ? `Ручной до ${schedule.override_until_time}` : mode.label; setPill($("#connection"), status.online ? "Online" : "Offline", status.online ? "good" : "bad"); setPill($("#mode-pill"), headerMode, mode.tone); setText("#mode-detail", mode.detail); updateSpeedVisual(tion.fan_speed); setText("#speed-note", status.auto?.state === "active" && status.auto?.target_speed ? `AUTO выбрал скорость ${status.auto.target_speed}` : `Ступень ${numberOrDash(tion.fan_speed)} из 6`); updateTemperatureVisual(tion.target_temp); setText("#heating-note", tion.heating ? "Сейчас нагревает входящий воздух" : tion.heater ? "Обогрев разрешён" : "Желаемая температура воздуха"); drawToggle("#power-toggle", tion.power); drawToggle("#heater-toggle", tion.heater); setText("#power-detail", tion.power ? "Устройство включено" : "Устройство выключено"); setText("#heater-detail", tion.heating ? "Сейчас нагревает" : tion.heater ? "Автоматически" : "Выключен"); setText("#co2", numberOrDash(sensor.co2)); setText("#air-label", quality.label); $("#air-label").className = `quality-chip ${toneClass(quality.tone)}`; $("#co2-progress").style.width = `${Math.max(0, Math.min(100, quality.pct))}%`; setText("#room-temp", numberOrDash(sensor.temperature ?? tion.in_temp, 1)); setText("#humidity", numberOrDash(sensor.humidity)); setText("#current-time", schedule.current_time || tion.device_time || "—"); setText("#next-time", schedule.next_time || "—"); } async function refresh(silent = false) { try { render(await api.status()); } catch (error) { setPill($("#connection"), "Ошибка", "bad"); if (!silent) showToast(error.message, true); } } async function act(path, source) { if (busy) return; busy = true; source?.setAttribute("disabled", ""); try { await api.post(path); await refresh(true); } catch (error) { showToast(error.message, true); await refresh(true); } finally { busy = false; source?.removeAttribute("disabled"); } } function stepped(kind, delta, source) { if (busy || !state?.tion) return; const isSpeed = kind === "speed"; const current = Number(isSpeed ? state.tion.fan_speed : state.tion.target_temp); const min = isSpeed ? 1 : 5; const max = isSpeed ? 6 : 30; const value = Math.min(max, Math.max(min, current + delta)); if (!Number.isFinite(value) || value === current) return; if (isSpeed) { state.tion.fan_speed = value; updateSpeedVisual(value); } else { state.tion.target_temp = value; updateTemperatureVisual(value); } act(`/api/tion/${isSpeed ? "speed" : "temperature"}/${value}`, source); } document.querySelectorAll("[data-step]").forEach(button => button.addEventListener("click", () => { stepped(button.dataset.step, Number(button.dataset.delta), button); })); document.querySelectorAll("[data-speed]").forEach(button => button.addEventListener("click", () => { if (busy || !state?.tion) return; const value = Number(button.dataset.speed); state.tion.fan_speed = value; updateSpeedVisual(value); act(`/api/tion/speed/${value}`, button); })); $("#temperature-range").addEventListener("input", event => setText("#target-temp", event.target.value)); $("#temperature-range").addEventListener("change", event => { if (busy || !state?.tion) return; const value = Number(event.target.value); state.tion.target_temp = value; updateTemperatureVisual(value); act(`/api/tion/temperature/${value}`, event.target); }); $("#power-toggle").addEventListener("click", event => { if (busy || !state?.tion) return; const value = !state.tion.power; state.tion.power = value; drawToggle("#power-toggle", value); setText("#power-detail", value ? "Устройство включено" : "Устройство выключено"); act(`/api/tion/power/${value ? "on" : "off"}`, event.currentTarget); }); $("#heater-toggle").addEventListener("click", event => { if (busy || !state?.tion) return; const value = !state.tion.heater; state.tion.heater = value; drawToggle("#heater-toggle", value); setText("#heater-detail", value ? "Автоматически" : "Выключен"); act(`/api/tion/heater/${value ? "on" : "off"}`, event.currentTarget); }); $("#open-panel").addEventListener("click", () => window.open(panelUrl(themeMode()), "_blank", "noopener")); $("#air-link").addEventListener("click", () => window.open(panelUrl(themeMode(), "air"), "_blank", "noopener")); $("#schedule-link").addEventListener("click", () => window.open(panelUrl(themeMode(), "schedule"), "_blank", "noopener")); refresh(); setInterval(() => !document.hidden && !busy && refresh(true), 3000);