478 lines
35 KiB
JavaScript
478 lines
35 KiB
JavaScript
const ADMIN_THEMES = new Set(["dark", "ultra_dark", "light", "cream"]);
|
|
const ADMIN_TIME_FORMATS = new Set(["24h", "12h"]);
|
|
const ADMIN_CHART_STYLES = new Set(["default", "soft", "technical", "high_contrast", "neon", "monochrome", "warm", "cool", "sunset", "aurora", "ocean", "forest", "pastel", "terminal", "blueprint", "colorblind"]);
|
|
const ADMIN_DENSITIES = new Set(["normal", "compact", "ultra"]);
|
|
const ADMIN_BUTTON_STYLES = new Set(["normal", "compact"]);
|
|
const ADMIN_CORNER_STYLES = new Set(["default", "square", "subtle", "rounded", "extra"]);
|
|
const ADMIN_DASHBOARD_BY_ID = new Map(ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => [widget.id, widget]));
|
|
const ADMIN_PRESET_ORDER = Object.freeze(ADMIN_DASHBOARD_DEFAULTS.dashboard_widgets.filter(id => ADMIN_DASHBOARD_BY_ID.has(id)));
|
|
const ADMIN_DASHBOARD_WIDGETS = Object.freeze([...new Set([...ADMIN_PRESET_ORDER, ...ADMIN_DASHBOARD_WIDGET_REGISTRY.map(widget => widget.id)])]);
|
|
const ADMIN_DEFAULT_HIDDEN_WIDGETS = Object.freeze([...new Set([
|
|
...ADMIN_DASHBOARD_DEFAULTS.dashboard_hidden_widgets.filter(id => ADMIN_DASHBOARD_BY_ID.has(id)),
|
|
...ADMIN_DASHBOARD_WIDGET_REGISTRY.filter(widget => widget.default_hidden && !ADMIN_PRESET_ORDER.includes(widget.id)).map(widget => widget.id),
|
|
])]);
|
|
const ADMIN_APPEARANCE_DEFAULTS = Object.freeze({
|
|
theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", chart_style: "default",
|
|
chart_axes: false, interface_icons: false, logo_animation: false,
|
|
button_style: "normal", corner_style: "default", table_density: "normal", dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_DEFAULT_HIDDEN_WIDGETS], dashboard_widget_sizes: null,
|
|
});
|
|
const CHART_STYLE_KEYS = Object.freeze({ default: "chartStyleDefault", soft: "chartStyleSoft", technical: "chartStyleTechnical", high_contrast: "chartStyleHighContrast", neon: "chartStyleNeon", monochrome: "chartStyleMonochrome", warm: "chartStyleWarm", cool: "chartStyleCool", sunset: "chartStyleSunset", aurora: "chartStyleAurora", ocean: "chartStyleOcean", forest: "chartStyleForest", pastel: "chartStylePastel", terminal: "chartStyleTerminal", blueprint: "chartStyleBlueprint", colorblind: "chartStyleColorblind" });
|
|
let adminPreferences = null, adminPreferencesLoading = null, adminPreferencesWrite = Promise.resolve(), adminPreferencesUnavailable = false;
|
|
|
|
function normalizeAdminTheme(value) { return ADMIN_THEMES.has(value) ? value : "dark"; }
|
|
function normalizeAdminLanguage(value) { return value === "auto" || /^[A-Za-z0-9_-]{1,64}$/.test(value || "") ? value : "auto"; }
|
|
function normalizeAdminUiScale(value) { const numeric = Number(value); return Number.isInteger(numeric) ? Math.min(125, Math.max(70, numeric)) : 100; }
|
|
function normalizeAdminTimeFormat(value) { return ADMIN_TIME_FORMATS.has(value) ? value : "24h"; }
|
|
function normalizeAdminChartStyle(value) { return ADMIN_CHART_STYLES.has(value) ? value : "default"; }
|
|
function normalizeAdminButtonStyle(value) { return ADMIN_BUTTON_STYLES.has(value) ? value : "normal"; }
|
|
function normalizeAdminCornerStyle(value) { return ADMIN_CORNER_STYLES.has(value) ? value : "default"; }
|
|
function normalizeAdminDensity(value, fallback = "normal") { return ADMIN_DENSITIES.has(value) ? value : fallback; }
|
|
function normalizeDashboardWidgets(value) {
|
|
const result = [], seen = new Set();
|
|
for (const id of Array.isArray(value) ? value : []) if (ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id)) { seen.add(id); result.push(id); }
|
|
for (const id of ADMIN_DASHBOARD_WIDGETS) if (!seen.has(id)) result.push(id);
|
|
return result;
|
|
}
|
|
function normalizeHiddenDashboardWidgets(value, savedOrder = ADMIN_DASHBOARD_WIDGETS) {
|
|
const seen = new Set();
|
|
const hidden = (Array.isArray(value) ? value : []).filter(id => ADMIN_DASHBOARD_WIDGETS.includes(id) && !seen.has(id) && seen.add(id));
|
|
for (const id of ADMIN_DEFAULT_HIDDEN_WIDGETS) {
|
|
if (!savedOrder?.includes(id) && !seen.has(id)) { hidden.push(id); seen.add(id); }
|
|
}
|
|
return hidden;
|
|
}
|
|
|
|
const DASHBOARD_WIDTHS = Object.freeze([2, 3, 4, 6, 12]);
|
|
function dashboardAllowedWidths(id) {
|
|
switch (dashboardWidgetType(id)) {
|
|
case "table": return [6, 12];
|
|
case "panel": return [4, 6, 12];
|
|
case "chart": return [3, 4, 6, 12];
|
|
default: return DASHBOARD_WIDTHS;
|
|
}
|
|
}
|
|
function dashboardDefaultWidth(id) {
|
|
const node = document.querySelector(`#dashboard-overview-grid [data-dashboard-widget="${id}"]`);
|
|
if (!node) return 2;
|
|
if (node.classList.contains("pool-card")) return 12;
|
|
if (node.classList.contains("overview-wide-chart")) return 6;
|
|
if (node.classList.contains("stat")) return 2;
|
|
return 4;
|
|
}
|
|
function normalizeDashboardWidgetSizes(value) {
|
|
if (value === null || value === undefined) return null;
|
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
const result = {};
|
|
for (const [id, span] of Object.entries(value)) {
|
|
if (ADMIN_DASHBOARD_BY_ID.has(id) && dashboardAllowedWidths(id).includes(span)
|
|
&& span !== dashboardDefaultWidth(id)) result[id] = span;
|
|
}
|
|
return result;
|
|
}
|
|
function dashboardWidgetWidth(id, sizes) {
|
|
return sizes?.[id] ?? dashboardDefaultWidth(id);
|
|
}
|
|
function dashboardVisualOrder(order, sizes = null) {
|
|
const normalized = normalizeDashboardWidgets(order);
|
|
if (sizes !== null) return normalized;
|
|
return normalized.map((id, index) => ({ id, index }))
|
|
.sort((a, b) => (window.rustProxyMasonry?.groupFor?.(a.id) ?? 0) -
|
|
(window.rustProxyMasonry?.groupFor?.(b.id) ?? 0) || a.index - b.index)
|
|
.map(item => item.id);
|
|
}
|
|
function browserLanguage() {
|
|
const packs = [...languagePacks.values()], candidates = (navigator.languages || [navigator.language]).filter(Boolean).map(value => value.replaceAll("_", "-").toLowerCase());
|
|
return candidates.map(value => packs.find(pack => pack.meta.locale.toLowerCase() === value || pack.id.replaceAll("_", "-").toLowerCase() === value) || packs.find(pack => pack.meta.locale.toLowerCase().split("-")[0] === value.split("-")[0])).find(Boolean)?.id || defaultLanguage;
|
|
}
|
|
function applyAdminTheme(value, cache = true) {
|
|
const theme = normalizeAdminTheme(value); document.documentElement.dataset.theme = theme;
|
|
const select = $("ui-theme"); if (select) select.value = theme;
|
|
if (cache) { try { localStorage.setItem("rp-theme", theme); } catch { } }
|
|
return theme;
|
|
}
|
|
function applyAdminLanguage(value, cache = true, rerender = true) {
|
|
const preference = normalizeAdminLanguage(value); languagePreference = preference;
|
|
if (languagePacks.size) language = preference === "auto" ? browserLanguage() : (languagePacks.has(preference) ? preference : browserLanguage());
|
|
const select = $("language"); if (select) select.value = preference === "auto" ? "auto" : language;
|
|
if (cache) { try { localStorage.setItem("rp-language", preference); } catch { } }
|
|
if (rerender && languagePacks.size) translate();
|
|
return preference;
|
|
}
|
|
function applyAdminUiScale(value, cache = true) {
|
|
const scale = normalizeAdminUiScale(value), root = document.documentElement, fieldScale = Math.min(1.1, Math.max(.9, 1 + (scale - 100) / 300));
|
|
root.style.setProperty("--ui-scale", String(scale / 100));
|
|
root.style.setProperty("--ui-field-font-size", `${(12 * fieldScale).toFixed(2)}px`);
|
|
root.style.setProperty("--ui-field-pad-y", `${(6 * fieldScale).toFixed(2)}px`);
|
|
root.style.setProperty("--ui-field-pad-x", `${(8 * fieldScale).toFixed(2)}px`);
|
|
root.style.setProperty("--ui-field-min-height", `${(27 * fieldScale).toFixed(2)}px`);
|
|
root.style.setProperty("--ui-check-size", `${(15 * fieldScale).toFixed(2)}px`);
|
|
root.style.setProperty("--ui-range-height", `${(18 * fieldScale).toFixed(2)}px`);
|
|
const slider = $("ui-scale"), output = $("ui-scale-value"); if (slider) slider.value = String(scale); if (output) output.textContent = `${scale}%`;
|
|
if (cache) { try { localStorage.setItem("rp-ui-scale", String(scale)); } catch { } }
|
|
return scale;
|
|
}
|
|
function applyAdminTimeFormat(value) { const format = normalizeAdminTimeFormat(value); const select = $("ui-time-format"); if (select) select.value = format; return format; }
|
|
function applyAdminChartStyle(value) {
|
|
const style = normalizeAdminChartStyle(value); document.documentElement.dataset.chartStyle = style;
|
|
const select = $("ui-chart-style"); if (select) select.value = style; refreshChartStylePalette(style); return style;
|
|
}
|
|
function applyAdminChartAxes(value) {
|
|
const enabled = value === true;
|
|
document.documentElement.dataset.chartAxes = String(enabled);
|
|
const input = $("ui-chart-axes"); if (input) input.checked = enabled;
|
|
window.rustProxyChartScale?.refreshAxes?.();
|
|
return enabled;
|
|
}
|
|
function applyAdminInterfaceIcons(value) { return window.rustProxyIcons?.setEnabled?.(value === true) ?? false; }
|
|
function applyAdminLogoAnimation(value) { return window.rustProxyLogoAnimation?.setEnabled?.(value === true) ?? false; }
|
|
function applyAdminButtonStyle(value) {
|
|
const style = normalizeAdminButtonStyle(value); document.documentElement.dataset.buttonStyle = style;
|
|
const select = $("ui-button-style"); if (select) select.value = style; return style;
|
|
}
|
|
function applyAdminCornerStyle(value, cache = true) {
|
|
const style = normalizeAdminCornerStyle(value);
|
|
document.documentElement.dataset.cornerStyle = style;
|
|
const select = $("ui-corner-style"); if (select) select.value = style;
|
|
if (cache) { try { localStorage.setItem("rp-corner-style", style); } catch { } }
|
|
return style;
|
|
}
|
|
function applyAdminTableDensity(value) {
|
|
const density = normalizeAdminDensity(value); document.documentElement.dataset.tableDensity = density;
|
|
const select = $("ui-table-density"); if (select) select.value = density; return density;
|
|
}
|
|
function applyAdminDashboardDensity(value) {
|
|
const density = normalizeAdminDensity(value, "compact"); document.documentElement.dataset.dashboardDensity = density;
|
|
const select = $("ui-dashboard-density"); if (select) select.value = density; return density;
|
|
}
|
|
function applyDashboardLayout(order, hidden, sizes = null) {
|
|
const normalizedOrder = normalizeDashboardWidgets(order), hiddenSet = new Set(normalizeHiddenDashboardWidgets(hidden));
|
|
const widths = normalizeDashboardWidgetSizes(sizes);
|
|
const grid = document.querySelector("#panel-overview .stats-grid");
|
|
if (!grid) return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
|
|
grid.dataset.overviewLayoutMode = widths === null ? "legacy" : "custom";
|
|
const nodes = new Map([...grid.querySelectorAll("[data-dashboard-widget]")].map(node => [node.dataset.dashboardWidget, node]));
|
|
const orderedNodes = normalizedOrder.map(id => nodes.get(id)).filter(Boolean);
|
|
for (const node of orderedNodes) {
|
|
node.hidden = hiddenSet.has(node.dataset.dashboardWidget);
|
|
node.dataset.dashboardSpan = String(dashboardWidgetWidth(node.dataset.dashboardWidget, widths));
|
|
}
|
|
const currentNodes = [...grid.querySelectorAll("[data-dashboard-widget]")];
|
|
if (orderedNodes.some((node, index) => node !== currentNodes[index])) grid.append(...orderedNodes);
|
|
window.rustProxyStreamDashboard?.syncVisibility?.([...hiddenSet]);
|
|
window.rustProxyBackendDashboard?.syncVisibility?.([...hiddenSet]);
|
|
window.rustProxyMasonry?.refresh?.();
|
|
if (typeof latestStatus !== "undefined" && latestStatus && currentPanel === "overview") { window.rustProxyStreamDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyBackendDashboard?.render?.(latestStatus, [...hiddenSet], false); window.rustProxyExtraOverview?.render?.(latestStatus, false); window.rustProxyWideOverview?.render?.(latestStatus, false); window.rustProxyWebOverview?.render?.(latestStatus, false); }
|
|
return { order: normalizedOrder, hidden: [...hiddenSet], sizes: widths };
|
|
}
|
|
function dashboardWidgetLabel(id) {
|
|
const widget = ADMIN_DASHBOARD_BY_ID.get(id);
|
|
return widget ? t(widget.label_key) : id;
|
|
}
|
|
const DASHBOARD_DISPLAY_TYPE_KEYS = Object.freeze({
|
|
value: "overviewBlockTypeValue", value_chart: "overviewBlockTypeValueChart",
|
|
chart: "overviewBlockTypeChart", panel: "overviewBlockTypePanel", table: "overviewBlockTypeTable",
|
|
});
|
|
function dashboardWidgetType(id) {
|
|
return ADMIN_DASHBOARD_BY_ID.get(id)?.display_type || "value";
|
|
}
|
|
function dashboardWidgetTypeLabel(id) {
|
|
return t(DASHBOARD_DISPLAY_TYPE_KEYS[dashboardWidgetType(id)] || "overviewBlockTypeValue");
|
|
}
|
|
function dashboardWidgetTypeBadge(id) {
|
|
const tag = el("span", dashboardWidgetTypeLabel(id), "overview-widget-type");
|
|
tag.dataset.blockType = dashboardWidgetType(id);
|
|
return tag;
|
|
}
|
|
function renderDashboardWidgetSettings() {
|
|
const list = $("dashboard-widget-list"); if (!list) return;
|
|
const previousScroll = list.scrollLeft;
|
|
const focusedRow = document.activeElement?.closest?.("[data-dashboard-widget-setting]");
|
|
const focusedId = focusedRow && list.contains?.(focusedRow) ? focusedRow.dataset.dashboardWidgetSetting : null;
|
|
const focusedAction = focusedId ? document.activeElement?.dataset.dashboardWidgetAction : null;
|
|
const current = adminPreferences || ADMIN_APPEARANCE_DEFAULTS, order = dashboardVisualOrder(current.dashboard_widgets, current.dashboard_widget_sizes), hidden = new Set(normalizeHiddenDashboardWidgets(current.dashboard_hidden_widgets));
|
|
const rows = order.map((id, index) => {
|
|
const row = el("div", undefined, "dashboard-widget-setting"); row.dataset.dashboardWidgetSetting = id;
|
|
const visible = !hidden.has(id);
|
|
const toggle = el("button", undefined, "dashboard-widget-toggle"); toggle.type = "button"; toggle.dataset.dashboardWidgetAction = "toggle"; toggle.classList.toggle("is-hidden", !visible); toggle.setAttribute("aria-pressed", String(visible)); toggle.setAttribute("aria-label", `${t("dashboardWidgetVisible")}: ${dashboardWidgetLabel(id)}`);
|
|
const descriptionKey = ADMIN_DASHBOARD_BY_ID.get(id)?.description_key;
|
|
if (descriptionKey) {
|
|
const description = t(descriptionKey);
|
|
row.title = description;
|
|
toggle.setAttribute("aria-description", description);
|
|
}
|
|
toggle.append(el("span", undefined, "dashboard-widget-visibility"), el("span", dashboardWidgetLabel(id), "dashboard-widget-setting-label"));
|
|
const actions = el("span", undefined, "dashboard-widget-setting-actions");
|
|
const up = el("button", "←", "quiet"); up.type = "button"; up.dataset.dashboardWidgetAction = "up"; up.disabled = index === 0; up.setAttribute("aria-label", t("dashboardWidgetsMoveEarlier"));
|
|
const down = el("button", "→", "quiet"); down.type = "button"; down.dataset.dashboardWidgetAction = "down"; down.disabled = index === order.length - 1; down.setAttribute("aria-label", t("dashboardWidgetsMoveLater"));
|
|
toggle.addEventListener("click", () => {
|
|
const previous = [...hidden], next = new Set(hidden); if (visible) next.add(id); else next.delete(id);
|
|
if (adminPreferences) adminPreferences.dashboard_hidden_widgets = [...next]; applyDashboardLayout(order, [...next], current.dashboard_widget_sizes); renderDashboardWidgetSettings();
|
|
void saveAppearancePreference({ dashboard_hidden_widgets: [...next] }, toggle, () => { if (adminPreferences) adminPreferences.dashboard_hidden_widgets = previous; applyDashboardLayout(order, previous, current.dashboard_widget_sizes); renderDashboardWidgetSettings(); });
|
|
});
|
|
const move = delta => {
|
|
const previous = [...order], next = [...order], at = next.indexOf(id), target = at + delta; if (at < 0 || target < 0 || target >= next.length) return;
|
|
[next[at], next[target]] = [next[target], next[at]];
|
|
const previousSizes = current.dashboard_widget_sizes;
|
|
if (adminPreferences) { adminPreferences.dashboard_widgets = next; adminPreferences.dashboard_widget_sizes = normalizeDashboardWidgetSizes(previousSizes) ?? {}; }
|
|
applyDashboardLayout(next, [...hidden], current.dashboard_widget_sizes); renderDashboardWidgetSettings();
|
|
void saveAppearancePreference({ dashboard_widgets: next, dashboard_widget_sizes: current.dashboard_widget_sizes }, null, () => {
|
|
if (adminPreferences) { adminPreferences.dashboard_widgets = previous; adminPreferences.dashboard_widget_sizes = previousSizes; }
|
|
applyDashboardLayout(previous, [...hidden], previousSizes); renderDashboardWidgetSettings();
|
|
});
|
|
};
|
|
up.addEventListener("click", () => move(-1)); down.addEventListener("click", () => move(1)); actions.append(up, down); row.append(toggle, actions); return row;
|
|
});
|
|
list.replaceChildren(...rows);
|
|
list.scrollLeft = previousScroll;
|
|
if (focusedId && focusedAction) {
|
|
const row = [...list.children].find(item => item.dataset.dashboardWidgetSetting === focusedId);
|
|
row?.querySelector(`[data-dashboard-widget-action="${focusedAction}"]`)?.focus({ preventScroll: true });
|
|
}
|
|
syncDashboardWidgetListView();
|
|
if (typeof requestAnimationFrame === "function") requestAnimationFrame(syncDashboardWidgetScrollButtons);
|
|
}
|
|
let dashboardWidgetListExpanded = false;
|
|
function syncDashboardWidgetListView() {
|
|
const list = $("dashboard-widget-list"), expand = $("dashboard-widget-expand");
|
|
if (!list || !expand) return;
|
|
list.classList.toggle("is-expanded", dashboardWidgetListExpanded);
|
|
expand.setAttribute("aria-expanded", String(dashboardWidgetListExpanded));
|
|
const key = dashboardWidgetListExpanded ? "dashboardWidgetsCompact" : "dashboardWidgetsShowAll";
|
|
expand.dataset.i18n = key;
|
|
expand.textContent = t(key);
|
|
for (const id of ["dashboard-widget-scroll-prev", "dashboard-widget-scroll-next"]) {
|
|
const button = $(id); if (button) button.hidden = dashboardWidgetListExpanded;
|
|
}
|
|
syncDashboardWidgetScrollButtons();
|
|
}
|
|
function syncDashboardWidgetScrollButtons() {
|
|
const list = $("dashboard-widget-list"); if (!list) return;
|
|
const previous = $("dashboard-widget-scroll-prev"), next = $("dashboard-widget-scroll-next");
|
|
if (previous) previous.disabled = dashboardWidgetListExpanded || list.scrollLeft <= 1;
|
|
if (next) next.disabled = dashboardWidgetListExpanded || list.scrollLeft + list.clientWidth >= list.scrollWidth - 1;
|
|
}
|
|
$("dashboard-widget-expand")?.addEventListener("click", () => {
|
|
dashboardWidgetListExpanded = !dashboardWidgetListExpanded;
|
|
syncDashboardWidgetListView();
|
|
});
|
|
$("dashboard-widget-list")?.addEventListener?.("scroll", syncDashboardWidgetScrollButtons, { passive: true });
|
|
window.addEventListener?.("resize", syncDashboardWidgetScrollButtons);
|
|
for (const [id, direction] of [["dashboard-widget-scroll-prev", -1], ["dashboard-widget-scroll-next", 1]]) {
|
|
$(id)?.addEventListener("click", () => {
|
|
const list = $("dashboard-widget-list");
|
|
if (!list) return;
|
|
list.scrollBy({ left: direction * Math.max(160, list.clientWidth * .8), behavior: "smooth" });
|
|
});
|
|
}
|
|
function refreshChartStylePalette(active = adminPreferences?.chart_style || "default") {
|
|
const target = $("chart-style-palette"); if (!target || !languagePacks.size) return;
|
|
const buttons = [...ADMIN_CHART_STYLES].map(style => {
|
|
const button = el("button", undefined, "chart-style-choice"); button.type = "button"; button.dataset.chartPreset = style; button.classList.toggle("active", style === active); button.setAttribute("aria-pressed", String(style === active));
|
|
const swatches = el("span", undefined, "chart-style-swatches"); for (let i = 1; i <= 4; i++) swatches.append(el("i"));
|
|
button.append(swatches, el("span", t(CHART_STYLE_KEYS[style]))); button.addEventListener("click", () => { const select = $("ui-chart-style"); if (!select) return; select.value = style; select.dispatchEvent(new Event("change", { bubbles: true })); }); return button;
|
|
});
|
|
target.replaceChildren(...buttons);
|
|
}
|
|
function refreshAppearanceUi() { refreshChartStylePalette(); renderDashboardWidgetSettings(); renderRefreshPreferences(adminPreferences?.refresh); window.rustProxyDashboardLayout?.refresh?.(); window.rustProxySettings?.refreshSearch?.(); }
|
|
function formatDateTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
|
function formatTime(value) { const date = value instanceof Date ? value : new Date(value); return date.toLocaleTimeString(locale(), { hour12: (adminPreferences?.time_format || "24h") === "12h" }); }
|
|
function applyAdminPreferences(value, cache = true) {
|
|
if (!value) return value;
|
|
|
|
const editing = window.rustProxyInlineLayout?.currentDraft?.();
|
|
const savedOrder = normalizeDashboardWidgets(value.dashboard_widgets);
|
|
const savedHidden = normalizeHiddenDashboardWidgets(value.dashboard_hidden_widgets, value.dashboard_widgets);
|
|
const savedSizes = normalizeDashboardWidgetSizes(value.dashboard_widget_sizes);
|
|
if (editing) applyDashboardLayout(editing.order, editing.hidden, editing.sizes);
|
|
else applyDashboardLayout(savedOrder, savedHidden, savedSizes);
|
|
const normalized = {
|
|
...value,
|
|
refresh: normalizeRefreshPreferences(value.refresh),
|
|
theme: applyAdminTheme(value.theme, cache), language: applyAdminLanguage(value.language, cache, false), ui_scale: applyAdminUiScale(value.ui_scale, cache),
|
|
time_format: applyAdminTimeFormat(value.time_format), chart_style: applyAdminChartStyle(value.chart_style), chart_axes: applyAdminChartAxes(value.chart_axes), interface_icons: applyAdminInterfaceIcons(value.interface_icons), logo_animation: applyAdminLogoAnimation(value.logo_animation), button_style: applyAdminButtonStyle(value.button_style), corner_style: applyAdminCornerStyle(value.corner_style, cache), table_density: applyAdminTableDensity(value.table_density),
|
|
dashboard_density: applyAdminDashboardDensity(value.dashboard_density), dashboard_widgets: savedOrder, dashboard_hidden_widgets: savedHidden, dashboard_widget_sizes: savedSizes,
|
|
};
|
|
adminPreferences = normalized;
|
|
window.rustProxyRefresh?.update(normalized.refresh);
|
|
renderRefreshPreferences(normalized.refresh);
|
|
window.rustProxyVhosts?.applyPreferences(normalized);
|
|
window.rustProxyDocker?.applyPreferences(normalized);
|
|
window.rustProxyRedirects?.applyPreferences(normalized);
|
|
window.rustProxyNotFound?.applyPreferences(normalized);
|
|
window.rustProxyStreams?.applyPreferences(normalized);
|
|
if (languagePacks.size) translate();
|
|
refreshAppearanceUi(); return normalized;
|
|
}
|
|
try { applyAdminTheme(localStorage.getItem("rp-theme") || "dark", false); } catch { applyAdminTheme("dark", false); }
|
|
try { applyAdminUiScale(localStorage.getItem("rp-ui-scale") || 100, false); } catch { applyAdminUiScale(100, false); }
|
|
try { applyAdminCornerStyle(localStorage.getItem("rp-corner-style") || "default", false); } catch { applyAdminCornerStyle("default", false); }
|
|
|
|
async function loadAdminPreferences(force = false) {
|
|
if (!authenticated) return adminPreferences;
|
|
if (adminPreferences && !force) return adminPreferences;
|
|
if (adminPreferencesLoading) return adminPreferencesLoading;
|
|
const epoch = authEpoch;
|
|
adminPreferencesLoading = (async () => {
|
|
try {
|
|
const value = await api("/api/preferences");
|
|
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
|
adminPreferencesUnavailable = false;
|
|
return applyAdminPreferences(value);
|
|
} catch (error) {
|
|
if (error.status === 401 || !authenticated || epoch !== authEpoch) throw error;
|
|
console.warn("Preferences unavailable; using defaults", error);
|
|
adminPreferencesUnavailable = true;
|
|
return applyAdminPreferences({ ...ADMIN_APPEARANCE_DEFAULTS, refresh: REFRESH_DEFAULTS });
|
|
}
|
|
})();
|
|
try { return await adminPreferencesLoading; } finally { adminPreferencesLoading = null; }
|
|
}
|
|
function updateAdminPreferences(patch) {
|
|
const epoch = authEpoch;
|
|
const task = adminPreferencesWrite.then(async () => {
|
|
if (!authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
|
|
const current = await loadAdminPreferences(adminPreferencesUnavailable);
|
|
if (adminPreferencesUnavailable) throw new Error("Cannot persist preferences while the preference service is unavailable");
|
|
if (!current || !authenticated || epoch !== authEpoch) throw new Error(t("auth.required"));
|
|
const next = { ...current, ...patch };
|
|
next.refresh = normalizeRefreshPreferences(next.refresh);
|
|
next.theme = normalizeAdminTheme(next.theme); next.language = normalizeAdminLanguage(next.language); next.ui_scale = normalizeAdminUiScale(next.ui_scale);
|
|
next.time_format = normalizeAdminTimeFormat(next.time_format); next.chart_style = normalizeAdminChartStyle(next.chart_style); next.chart_axes = next.chart_axes === true; next.interface_icons = next.interface_icons === true; next.logo_animation = next.logo_animation === true; next.button_style = normalizeAdminButtonStyle(next.button_style); next.corner_style = normalizeAdminCornerStyle(next.corner_style); next.table_density = normalizeAdminDensity(next.table_density);
|
|
next.dashboard_density = normalizeAdminDensity(next.dashboard_density, "compact"); next.dashboard_widgets = normalizeDashboardWidgets(next.dashboard_widgets); next.dashboard_hidden_widgets = normalizeHiddenDashboardWidgets(next.dashboard_hidden_widgets); next.dashboard_widget_sizes = normalizeDashboardWidgetSizes(next.dashboard_widget_sizes);
|
|
const saved = await api("/api/preferences", { method: "PUT", body: JSON.stringify(next) });
|
|
if (!authenticated || epoch !== authEpoch) return adminPreferences;
|
|
return applyAdminPreferences(saved);
|
|
});
|
|
adminPreferencesWrite = task.catch(() => undefined); return task;
|
|
}
|
|
function resetAdminPreferences() { adminPreferences = null; adminPreferencesLoading = null; adminPreferencesUnavailable = false; adminPreferencesWrite = Promise.resolve(); applyAdminInterfaceIcons(false); applyAdminLogoAnimation(false); window.rustProxyRefresh?.update(REFRESH_DEFAULTS); }
|
|
function renderRefreshPreferences(value = REFRESH_DEFAULTS) {
|
|
const refresh = normalizeRefreshPreferences(value);
|
|
const switcher = $("refresh-live-enabled"); if (switcher) switcher.checked = refresh.live_enabled;
|
|
for (const field of Object.keys(REFRESH_CHOICES)) {
|
|
const input = $(`refresh-${field.replaceAll("_", "-")}`);
|
|
if (input) input.value = String(refresh[field]);
|
|
}
|
|
const manual = refresh.system_info_seconds === 0 || !refresh.live_enabled;
|
|
const hint = $("system-info-refresh-note");
|
|
if (hint) hint.textContent = t(manual ? "refreshManualHint" : "refreshSystemHint");
|
|
}
|
|
for (const control of document.querySelectorAll("[data-refresh-preference]")) {
|
|
control.addEventListener("change", () => {
|
|
const key = control.dataset.refreshPreference;
|
|
const previous = normalizeRefreshPreferences(adminPreferences?.refresh);
|
|
const value = key === "live_enabled" ? control.checked : Number(control.value);
|
|
if (key !== "live_enabled" && !REFRESH_CHOICES[key]?.includes(value)) {
|
|
renderRefreshPreferences(previous); return;
|
|
}
|
|
const next = { ...previous, [key]: value };
|
|
if (adminPreferences) adminPreferences.refresh = next;
|
|
window.rustProxyRefresh?.update(next); renderRefreshPreferences(next);
|
|
void saveAppearancePreference({ refresh: next }, control, () => {
|
|
if (adminPreferences) adminPreferences.refresh = previous;
|
|
window.rustProxyRefresh?.update(previous); renderRefreshPreferences(previous);
|
|
});
|
|
});
|
|
}
|
|
$("refresh-reset")?.addEventListener("click", () => {
|
|
const previous = normalizeRefreshPreferences(adminPreferences?.refresh), next = { ...REFRESH_DEFAULTS };
|
|
if (adminPreferences) adminPreferences.refresh = next;
|
|
window.rustProxyRefresh?.update(next); renderRefreshPreferences(next);
|
|
void saveAppearancePreference({ refresh: next }, $("refresh-reset"), () => {
|
|
if (adminPreferences) adminPreferences.refresh = previous;
|
|
window.rustProxyRefresh?.update(previous); renderRefreshPreferences(previous);
|
|
});
|
|
});
|
|
window.rustProxyPreferences = { load: loadAdminPreferences, update: updateAdminPreferences, current: () => adminPreferences, applyTheme: applyAdminTheme, refreshAppearanceUi };
|
|
|
|
async function saveAppearancePreference(patch, control, rollback) {
|
|
if (control) control.disabled = true;
|
|
try { await updateAdminPreferences(patch); }
|
|
catch (error) { rollback?.(); if (error.status !== 401) toast(error.message, true); }
|
|
finally { if (control) control.disabled = false; }
|
|
}
|
|
$("language")?.addEventListener("change", () => {
|
|
const control = $("language"), previous = adminPreferences?.language || "auto", next = normalizeAdminLanguage(control.value);
|
|
applyAdminLanguage(next); void saveAppearancePreference({ language: next }, control, () => applyAdminLanguage(previous));
|
|
});
|
|
$("ui-theme")?.addEventListener("change", () => {
|
|
const control = $("ui-theme"), previous = adminPreferences?.theme || "dark", next = normalizeAdminTheme(control.value);
|
|
applyAdminTheme(next); void saveAppearancePreference({ theme: next }, control, () => applyAdminTheme(previous));
|
|
});
|
|
$("ui-scale")?.addEventListener("input", () => applyAdminUiScale($("ui-scale").value));
|
|
$("ui-scale")?.addEventListener("change", () => {
|
|
const control = $("ui-scale"), previous = adminPreferences?.ui_scale || 100, next = normalizeAdminUiScale(control.value);
|
|
void saveAppearancePreference({ ui_scale: next }, control, () => applyAdminUiScale(previous));
|
|
});
|
|
$("ui-time-format")?.addEventListener("change", () => {
|
|
const control = $("ui-time-format"), previous = adminPreferences?.time_format || "24h", next = normalizeAdminTimeFormat(control.value);
|
|
applyAdminTimeFormat(next); if (adminPreferences) adminPreferences.time_format = next; translate();
|
|
void saveAppearancePreference({ time_format: next }, control, () => { if (adminPreferences) adminPreferences.time_format = previous; applyAdminTimeFormat(previous); translate(); });
|
|
});
|
|
$("ui-chart-style")?.addEventListener("change", () => {
|
|
const control = $("ui-chart-style"), previous = adminPreferences?.chart_style || "default", next = normalizeAdminChartStyle(control.value);
|
|
applyAdminChartStyle(next); if (adminPreferences) adminPreferences.chart_style = next; void saveAppearancePreference({ chart_style: next }, control, () => { if (adminPreferences) adminPreferences.chart_style = previous; applyAdminChartStyle(previous); });
|
|
});
|
|
$("ui-chart-axes")?.addEventListener("change", () => {
|
|
const control = $("ui-chart-axes"), previous = adminPreferences?.chart_axes === true, next = control.checked;
|
|
applyAdminChartAxes(next); if (adminPreferences) adminPreferences.chart_axes = next;
|
|
void saveAppearancePreference({ chart_axes: next }, control, () => {
|
|
if (adminPreferences) adminPreferences.chart_axes = previous;
|
|
applyAdminChartAxes(previous);
|
|
});
|
|
});
|
|
$("ui-interface-icons")?.addEventListener("change", () => {
|
|
const control = $("ui-interface-icons"), previous = adminPreferences?.interface_icons === true, next = control.checked;
|
|
applyAdminInterfaceIcons(next); if (adminPreferences) adminPreferences.interface_icons = next;
|
|
void saveAppearancePreference({ interface_icons: next }, control, () => {
|
|
if (adminPreferences) adminPreferences.interface_icons = previous;
|
|
applyAdminInterfaceIcons(previous);
|
|
});
|
|
});
|
|
$("ui-logo-animation")?.addEventListener("change", () => {
|
|
const control = $("ui-logo-animation"), previous = adminPreferences?.logo_animation === true, next = control.checked;
|
|
applyAdminLogoAnimation(next); if (adminPreferences) adminPreferences.logo_animation = next;
|
|
void saveAppearancePreference({ logo_animation: next }, control, () => {
|
|
if (adminPreferences) adminPreferences.logo_animation = previous;
|
|
applyAdminLogoAnimation(previous);
|
|
});
|
|
});
|
|
$("ui-button-style")?.addEventListener("change", () => {
|
|
const control = $("ui-button-style"), previous = adminPreferences?.button_style || "normal", next = normalizeAdminButtonStyle(control.value);
|
|
applyAdminButtonStyle(next); if (adminPreferences) adminPreferences.button_style = next; void saveAppearancePreference({ button_style: next }, control, () => { if (adminPreferences) adminPreferences.button_style = previous; applyAdminButtonStyle(previous); });
|
|
});
|
|
$("ui-corner-style")?.addEventListener("change", () => {
|
|
const control = $("ui-corner-style"), previous = adminPreferences?.corner_style || "default", next = normalizeAdminCornerStyle(control.value);
|
|
applyAdminCornerStyle(next);
|
|
if (adminPreferences) adminPreferences.corner_style = next;
|
|
void saveAppearancePreference({ corner_style: next }, control, () => {
|
|
if (adminPreferences) adminPreferences.corner_style = previous;
|
|
applyAdminCornerStyle(previous);
|
|
});
|
|
});
|
|
$("ui-table-density")?.addEventListener("change", () => {
|
|
const control = $("ui-table-density"), previous = adminPreferences?.table_density || "normal", next = normalizeAdminDensity(control.value);
|
|
applyAdminTableDensity(next); if (adminPreferences) adminPreferences.table_density = next; void saveAppearancePreference({ table_density: next }, control, () => { if (adminPreferences) adminPreferences.table_density = previous; applyAdminTableDensity(previous); });
|
|
});
|
|
$("ui-dashboard-density")?.addEventListener("change", () => {
|
|
const control = $("ui-dashboard-density"), previous = adminPreferences?.dashboard_density || "compact", next = normalizeAdminDensity(control.value, "compact");
|
|
applyAdminDashboardDensity(next); if (adminPreferences) adminPreferences.dashboard_density = next; void saveAppearancePreference({ dashboard_density: next }, control, () => { if (adminPreferences) adminPreferences.dashboard_density = previous; applyAdminDashboardDensity(previous); });
|
|
});
|
|
async function resetAppearancePatch(patch, control, messageKey = "appearanceSectionResetDone") {
|
|
if (control) control.disabled = true; let previous = null;
|
|
try {
|
|
const current = await loadAdminPreferences(); previous = current ? { ...current, dashboard_widgets: [...current.dashboard_widgets], dashboard_hidden_widgets: [...current.dashboard_hidden_widgets], dashboard_widget_sizes: current.dashboard_widget_sizes === null ? null : { ...current.dashboard_widget_sizes } } : null;
|
|
if (current) applyAdminPreferences({ ...current, ...patch }); await updateAdminPreferences(patch); toast(t(messageKey));
|
|
} catch (error) { if (previous) applyAdminPreferences(previous); if (error.status !== 401) toast(error.message, true); }
|
|
finally { if (control) control.disabled = false; }
|
|
}
|
|
document.querySelectorAll("[data-appearance-reset]").forEach(control => control.addEventListener("click", () => {
|
|
const patches = {
|
|
interface: { theme: "dark", language: "auto", ui_scale: 100, time_format: "24h", interface_icons: false, logo_animation: false, button_style: "normal", corner_style: "default" }, charts: { chart_style: "default", chart_axes: false }, tables: { table_density: "normal" },
|
|
dashboard: { dashboard_density: ADMIN_DASHBOARD_DEFAULTS.dashboard_density, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null },
|
|
};
|
|
void resetAppearancePatch(patches[control.dataset.appearanceReset] || {}, control);
|
|
}));
|
|
$("appearance-reset")?.addEventListener("click", () => void resetAppearancePatch({ ...ADMIN_APPEARANCE_DEFAULTS, dashboard_widgets: [...ADMIN_DASHBOARD_WIDGETS], dashboard_hidden_widgets: [...ADMIN_APPEARANCE_DEFAULTS.dashboard_hidden_widgets], dashboard_widget_sizes: null }, $("appearance-reset"), "appearanceResetDone"));
|
|
refreshAppearanceUi();
|