Submit GUI commands without page navigation

This commit is contained in:
2026-08-03 02:04:17 +02:00
parent d7d472d9e1
commit 0da5377ff2
9 changed files with 346 additions and 37 deletions
+170 -5
View File
@@ -129,6 +129,174 @@ function refreshSnapshot(button) {
}, 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)) {
@@ -146,13 +314,10 @@ document.addEventListener("submit", (event) => {
handleRefreshSubmit(form, event);
return;
}
if (form.dataset.requiresAck !== "true") {
if (form.classList.contains("command-form")) {
handleCommandSubmit(form, event);
return;
}
const message = form.dataset.ackMessage || "Confirm this action";
if (!window.confirm(message)) {
event.preventDefault();
}
});
updateTimers();