window.rustProxyInlineLayout = (() => { const grid = $("dashboard-overview-grid"); const unlock = $("overview-layout-unlock"); const actions = $("overview-inline-actions"); const saveButton = $("overview-inline-save"); const cancelButton = $("overview-inline-cancel"); const hint = $("overview-inline-hint"); const hiddenShelf = $("overview-inline-hidden"); const hiddenList = $("overview-inline-hidden-list"); const hiddenCount = $("overview-inline-hidden-count"); const modalButton = $("overview-layout-open"); let draft = null; let original = null; let dragging = null; let opening = false; let saving = false; const visibleIds = () => dashboardVisualOrder(draft.order, draft.sizes) .filter(id => !draft.hidden.has(id)); function enterFreeLayout() { if (draft.sizes === null) { draft.order = dashboardVisualOrder(draft.order, null); draft.sizes = {}; } } const nodes = () => [...grid.children].filter(node => node.dataset.dashboardWidget); const nodeFor = id => nodes().find(node => node.dataset.dashboardWidget === id); const label = id => dashboardWidgetLabel(id); const dirty = () => draft && original && (draft.order.some((id, i) => id !== original.order[i]) || [...draft.hidden].some(id => !original.hidden.has(id)) || [...original.hidden].some(id => !draft.hidden.has(id)) || JSON.stringify(draft.sizes) !== JSON.stringify(original.sizes)); function renderHidden() { if (!hiddenList || !draft) return; const entries = draft.order.filter(id => draft.hidden.has(id)); if (hiddenCount) hiddenCount.textContent = `(${entries.length})`; hiddenList.replaceChildren(...(entries.length ? entries.map(id => { const add = control("+", `${t("overviewLayoutInlineRestore")}: ${label(id)}`, "overview-inline-restore", () => { draft.hidden.delete(id); paint(); nodeFor(id)?.querySelector(".overview-inline-grip")?.focus(); }); const item = el("div", undefined, "overview-inline-hidden-item"); item.append(el("span", label(id)), dashboardWidgetTypeBadge(id), add); return item; }) : [el("span", t("overviewLayoutInlineHiddenEmpty"), "fine")])); } function hide(id) { if (!draft || !visibleIds().includes(id)) return; draft.hidden.add(id); paint(); if (hiddenShelf) { hiddenShelf.open = true; hiddenShelf.querySelector("summary")?.focus(); } } function paint() { applyDashboardLayout(draft.order, [...draft.hidden], draft.sizes); renderHidden(); const visible = visibleIds(); for (const node of nodes()) { const i = visible.indexOf(node.dataset.dashboardWidget); const up = node.querySelector(".overview-inline-up"); const down = node.querySelector(".overview-inline-down"); if (up) up.disabled = i <= 0; if (down) down.disabled = i < 0 || i === visible.length - 1; const widths = dashboardAllowedWidths(node.dataset.dashboardWidget); const span = dashboardWidgetWidth(node.dataset.dashboardWidget, draft.sizes); const at = widths.indexOf(span); const value = node.querySelector(".overview-inline-width-value"); if (value) value.textContent = `${span}/12`; const narrow = node.querySelector(".overview-inline-narrow"); const widen = node.querySelector(".overview-inline-widen"); if (narrow) narrow.disabled = at <= 0; if (widen) widen.disabled = at === widths.length - 1; } } function clearDrag() { dragging = null; for (const node of nodes()) node.classList.remove("is-overview-dragging", "is-overview-drop-before", "is-overview-drop-after"); } function move(id, target, after = false) { if (!draft || id === target) return; if (!visibleIds().includes(id) || !visibleIds().includes(target)) return; enterFreeLayout(); const visible = visibleIds(); visible.splice(visible.indexOf(id), 1); visible.splice(visible.indexOf(target) + (after ? 1 : 0), 0, id); const iter = visible.values(); draft.order = draft.order.map(item => draft.hidden.has(item) ? item : iter.next().value); paint(); } function resize(id, delta) { if (!draft) return; const widths = dashboardAllowedWidths(id); const next = widths[widths.indexOf(dashboardWidgetWidth(id, draft.sizes)) + delta]; if (next === undefined) return; enterFreeLayout(); if (next === dashboardDefaultWidth(id)) delete draft.sizes[id]; else draft.sizes[id] = next; paint(); nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-narrow" : ".overview-inline-widen")?.focus(); } function nudge(id, delta) { const visible = visibleIds(); const index = visible.indexOf(id), target = visible[index + delta]; if (!target) return; move(id, target, delta > 0); nodeFor(id)?.querySelector(delta < 0 ? ".overview-inline-up" : ".overview-inline-down")?.focus(); } function control(text, title, className, run) { const button = el("button", text, `quiet ${className}`); button.type = "button"; button.setAttribute("aria-label", title); button.title = title; button.addEventListener("click", run); return button; } function toolbar(node) { const id = node.dataset.dashboardWidget; const title = label(id); const bar = el("div", undefined, "overview-inline-toolbar"); const grip = control("⠿", `${t("overviewLayoutInlineDrag")}: ${title}`, "overview-inline-grip", () => { }); grip.draggable = true; grip.dataset.overviewDragHandle = id; const sizeControls = el("span", undefined, "overview-inline-width"); const widthValue = el("span", "", "overview-inline-width-value"); widthValue.setAttribute("aria-label", t("overviewLayoutWidth")); sizeControls.append( control("−", `${t("overviewLayoutNarrow")}: ${title}`, "overview-inline-narrow", () => resize(id, -1)), widthValue, control("+", `${t("overviewLayoutWiden")}: ${title}`, "overview-inline-widen", () => resize(id, 1))); bar.append(grip, dashboardWidgetTypeBadge(id), sizeControls, control("←", `${t("moveUp")}: ${title}`, "overview-inline-up", () => nudge(id, -1)), control("→", `${t("moveDown")}: ${title}`, "overview-inline-down", () => nudge(id, 1)), control("×", `${t("overviewLayoutInlineHide")}: ${title}`, "overview-inline-hide", () => hide(id))); node.append(bar); } function showEditing() { grid.classList.add("is-unlocked"); unlock.setAttribute("aria-pressed", "true"); unlock.dataset.i18n = "overviewLayoutLock"; unlock.textContent = t("overviewLayoutLock"); unlock.hidden = true; // Save and lock is the only save action while unlocked. actions.hidden = false; hint.hidden = false; hint.textContent = t("overviewLayoutInlineHint"); if (hiddenShelf) { hiddenShelf.hidden = false; hiddenShelf.open = false; } if (modalButton) modalButton.disabled = true; for (const node of nodes()) toolbar(node); paint(); } function finish(preserveSaved = false) { clearDrag(); for (const node of nodes()) node.querySelector(".overview-inline-toolbar")?.remove(); grid.classList.remove("is-unlocked"); unlock.setAttribute("aria-pressed", "false"); unlock.dataset.i18n = "overviewLayoutUnlock"; unlock.textContent = t("overviewLayoutUnlock"); unlock.hidden = false; actions.hidden = true; hint.hidden = true; if (hiddenShelf) { hiddenShelf.open = false; hiddenShelf.hidden = true; } if (hiddenList) hiddenList.replaceChildren(); if (modalButton) modalButton.disabled = false; if (!preserveSaved && original) applyDashboardLayout(original.order, [...original.hidden], original.sizes); draft = original = null; } async function begin() { if (draft || opening || saving || $("overview-presets-dialog")?.open) return; opening = true; unlock.disabled = true; try { await window.rustProxyPreferences?.load?.(); const saved = window.rustProxyPreferences?.current?.() || ADMIN_APPEARANCE_DEFAULTS; const order = normalizeDashboardWidgets(saved.dashboard_widgets); const hidden = new Set(normalizeHiddenDashboardWidgets(saved.dashboard_hidden_widgets, saved.dashboard_widgets)); const sizes = normalizeDashboardWidgetSizes(saved.dashboard_widget_sizes); original = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } }; draft = { order: [...order], hidden: new Set(hidden), sizes: sizes === null ? null : { ...sizes } }; showEditing(); } catch (error) { if (error?.status !== 401) toast(error.message, true); } finally { opening = false; unlock.disabled = false; } } async function save() { if (!draft || saving) return; if (!dirty()) { finish(false); return; } saving = true; saveButton.disabled = cancelButton.disabled = unlock.disabled = true; try { await window.rustProxyPreferences.update({ dashboard_widgets: [...draft.order], dashboard_hidden_widgets: [...draft.hidden], dashboard_widget_sizes: draft.sizes === null ? null : { ...draft.sizes }, }); finish(true); toast(t("overviewLayoutEditorSaved")); } catch (error) { hint.textContent = error.message || t("overviewLayoutInlineError"); } finally { saving = false; saveButton.disabled = cancelButton.disabled = unlock.disabled = false; } } function cancel() { if (draft && !saving) finish(false); } function dropTarget(event) { const widget = event.target?.closest?.("[data-dashboard-widget]"); return widget && grid.contains(widget) && !widget.hidden && visibleIds().includes(widget.dataset.dashboardWidget) ? widget : null; } function positionAfter(widget, event) { const rect = widget.getBoundingClientRect(); return event.clientX >= rect.left + rect.width / 2; } grid?.addEventListener("dragstart", event => { const handle = event.target?.closest?.("[data-overview-drag-handle]"); if (!draft || !handle || !grid.contains(handle)) { event.preventDefault(); return; } dragging = handle.dataset.overviewDragHandle; event.dataTransfer?.setData("text/plain", dragging); if (event.dataTransfer) event.dataTransfer.effectAllowed = "move"; nodeFor(dragging)?.classList.add("is-overview-dragging"); }); grid?.addEventListener("dragover", event => { if (!draft || !dragging) return; const target = dropTarget(event); if (!target || target.dataset.dashboardWidget === dragging) return; event.preventDefault(); for (const node of nodes()) node.classList.remove("is-overview-drop-before", "is-overview-drop-after"); target.classList.add(positionAfter(target, event) ? "is-overview-drop-after" : "is-overview-drop-before"); if (event.dataTransfer) event.dataTransfer.dropEffect = "move"; }); grid?.addEventListener("drop", event => { if (!draft || !dragging) return; event.preventDefault(); const target = dropTarget(event), from = dragging; clearDrag(); if (target && target.dataset.dashboardWidget !== from) move(from, target.dataset.dashboardWidget, positionAfter(target, event)); }); grid?.addEventListener("dragend", clearDrag); unlock?.addEventListener("click", () => { if (draft) void save(); else void begin(); }); saveButton?.addEventListener("click", () => { void save(); }); cancelButton?.addEventListener("click", cancel); return { isEditing: () => !!draft, currentDraft: () => draft ? { order: [...draft.order], hidden: [...draft.hidden], sizes: draft.sizes === null ? null : { ...draft.sizes }, } : null, cancel, }; })();