const SNAPSHOT_REFRESH_DELAY_MS = 5000; function formatRemaining(totalSeconds) { const seconds = Math.max(0, Math.ceil(totalSeconds)); const minutes = Math.floor(seconds / 60); const rest = seconds % 60; return `${minutes}:${String(rest).padStart(2, "0")}`; } function updateTimers() { const now = Date.now() / 1000; document.querySelectorAll("[data-auto-off-at]").forEach((item) => { const target = Number(item.dataset.autoOffAt || 0); if (!target) { return; } const remaining = target - now; if (remaining <= 0) { item.textContent = item.dataset.expiredLabel || "Automatic turn-off pending"; item.classList.add("timer-expired"); return; } item.textContent = `${item.dataset.prefix || "Automatic turn-off in"} ${formatRemaining(remaining)}`; }); } function refreshStatusElement() { return document.querySelector("[data-refresh-status]"); } function showRefreshMessage(message, type = "success") { const status = refreshStatusElement(); if (!status) { return; } status.textContent = message || ""; status.classList.toggle("is-visible", Boolean(message)); status.classList.toggle("is-success", type === "success"); status.classList.toggle("is-error", type === "error"); } function rememberRefreshMessage(message) { try { window.sessionStorage.setItem("frigateRefreshMessage", message); } catch (_error) { // Some browsers may block sessionStorage; the refresh still succeeds. } } function restoreRefreshMessage() { try { const message = window.sessionStorage.getItem("frigateRefreshMessage"); if (!message) { return; } window.sessionStorage.removeItem("frigateRefreshMessage"); showRefreshMessage(message, "success"); } catch (_error) { // Ignore storage errors and keep the GUI usable. } } async function handleRefreshSubmit(form, event) { event.preventDefault(); const button = form.querySelector(".refresh-button"); if (button && button.classList.contains("is-refreshing")) { return; } const status = refreshStatusElement(); const workingLabel = status?.dataset.workingLabel || "Refreshing cameras..."; showRefreshMessage(workingLabel, "success"); if (button) { button.disabled = true; button.classList.add("is-refreshing"); button.setAttribute("aria-busy", "true"); } try { const response = await fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, }); const payload = await response.json(); if (!response.ok || payload.ok === false) { throw new Error(payload.message || "Refresh failed"); } showRefreshMessage(payload.message, "success"); if (payload.changed && payload.redirect_url) { rememberRefreshMessage(payload.message); window.setTimeout(() => { window.location.assign(payload.redirect_url); }, 900); return; } } catch (error) { showRefreshMessage(error.message || "Refresh failed", "error"); } finally { if (button) { button.disabled = false; button.classList.remove("is-refreshing"); button.removeAttribute("aria-busy"); } } } function refreshSnapshot(button) { if (button.classList.contains("is-refreshing")) { return; } const media = button.closest(".media"); const image = media?.querySelector("img.snapshot"); if (!image) { return; } const url = new URL(image.currentSrc || image.src, window.location.href); url.searchParams.set("refresh", "1"); url.searchParams.set("v", String(Date.now())); const cleanup = () => { button.disabled = false; button.classList.remove("is-refreshing"); button.removeAttribute("aria-busy"); }; button.disabled = true; button.classList.add("is-refreshing"); button.setAttribute("aria-busy", "true"); image.addEventListener("load", cleanup, { once: true }); image.addEventListener("error", cleanup, { once: true }); window.setTimeout(() => { image.src = url.toString(); }, SNAPSHOT_REFRESH_DELAY_MS); } function actionContainerById(actionId) { for (const container of document.querySelectorAll("[data-action-id]")) { if (container.dataset.actionId === actionId) { return container; } } return null; } function actionStatusForForm(form) { return form.closest(".action-control, .global-action")?.querySelector("[data-action-status]"); } function showActionMessage(form, message, type = "success") { const status = actionStatusForForm(form); if (!status) { return; } status.textContent = message || ""; status.classList.toggle("is-visible", Boolean(message)); status.classList.toggle("is-success", type === "success"); status.classList.toggle("is-error", type === "error"); } function setCommandBusy(button, busy) { if (!button) { return; } if (busy) { button.disabled = true; button.classList.add("is-working"); button.setAttribute("aria-busy", "true"); return; } if (button.dataset.timerLocked !== "true") { button.disabled = false; } button.classList.remove("is-working"); button.removeAttribute("aria-busy"); } function updateStateChip(container, action) { const chip = container.querySelector("[data-state-chip]"); if (!chip || !action.state) { return; } const state = String(action.state).toLowerCase(); for (const className of Array.from(chip.classList)) { if (className.startsWith("chip-state-")) { chip.classList.remove(className); } } chip.classList.add(`chip-state-${state}`); chip.textContent = action.state_label || chip.dataset[`label${state.charAt(0).toUpperCase()}${state.slice(1)}`] || action.state; } function updateTimer(container, action) { const timer = container.querySelector("[data-timer]"); if (!timer) { return; } if (action.auto_off_at) { timer.dataset.autoOffAt = String(action.auto_off_at); timer.classList.remove("timer-idle", "timer-expired"); updateTimers(); return; } delete timer.dataset.autoOffAt; timer.classList.add("timer-idle"); timer.classList.remove("timer-expired"); timer.textContent = timer.dataset.idleLabel || ""; } function updateOnButtonLock(container, action) { const button = container.querySelector(".cmd-on"); if (!button) { return; } if (action.auto_off_at) { const label = button.dataset.disabledLabel || "Already active until automatic turn-off"; button.dataset.timerLocked = "true"; button.disabled = true; button.title = label; button.setAttribute("aria-label", label); return; } delete button.dataset.timerLocked; if (!button.classList.contains("is-working")) { button.disabled = false; } button.removeAttribute("title"); button.removeAttribute("aria-label"); } function applyActionState(action) { if (!action?.id) { return; } const container = actionContainerById(action.id); if (!container) { return; } updateStateChip(container, action); updateTimer(container, action); updateOnButtonLock(container, action); } function applyActionPayload(payload) { if (Array.isArray(payload.actions)) { payload.actions.forEach(applyActionState); return; } applyActionState(payload.action); } function refreshSnapshotForCommand(form) { const button = form.closest(".camera-card")?.querySelector(".snapshot-refresh"); if (!button || button.classList.contains("is-refreshing")) { return; } refreshSnapshot(button); } async function responseJson(response) { const text = await response.text(); if (!text) { return {}; } try { return JSON.parse(text); } catch (_error) { return { ok: false, message: text }; } } async function handleCommandSubmit(form, event) { event.preventDefault(); if (form.dataset.requiresAck === "true") { const message = form.dataset.ackMessage || "Confirm this action"; if (!window.confirm(message)) { return; } } const button = form.querySelector(".cmd"); if (button?.classList.contains("is-working")) { return; } showActionMessage(form, form.dataset.workingLabel || "Sending command...", "success"); setCommandBusy(button, true); try { const response = await fetch(form.action, { method: "POST", headers: { Accept: "application/json" }, }); const payload = await responseJson(response); if (!response.ok || payload.ok === false) { throw new Error(payload.message || form.dataset.failedLabel || "Command failed."); } applyActionPayload(payload); showActionMessage(form, payload.message || form.dataset.successLabel || "Command sent.", "success"); refreshSnapshotForCommand(form); } catch (error) { showActionMessage(form, error.message || form.dataset.failedLabel || "Command failed.", "error"); } finally { setCommandBusy(button, false); } } document.addEventListener("click", (event) => { const button = event.target.closest(".snapshot-refresh"); if (!(button instanceof HTMLButtonElement)) { return; } refreshSnapshot(button); }); document.addEventListener("submit", (event) => { const form = event.target; if (!(form instanceof HTMLFormElement)) { return; } if (form.classList.contains("refresh-form")) { handleRefreshSubmit(form, event); return; } if (form.classList.contains("command-form")) { handleCommandSubmit(form, event); return; } }); updateTimers(); restoreRefreshMessage(); setInterval(updateTimers, 1000);