From 3dc8fdae051f5ab3f6459e1b1927ade65d46b11d Mon Sep 17 00:00:00 2001 From: mrleo1nid Date: Fri, 4 Sep 2026 20:36:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9A=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20=C2=AB?= =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B8=D1=82=D1=8C=C2=BB=20?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BD=D0=B0=20=D1=82=D0=BE=D0=BB?= =?UTF-8?q?=D1=8C=D0=BA=D0=BE=20=D0=BF=D1=80=D0=B8=20=D0=BD=D0=B5=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D1=91=D0=BD=D0=BD=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше она предлагала сохранить даже пустую вкладку: на свежей установке конфигов ещё нет, полей на странице ноль, а кнопка зовёт нажать себя. Теперь на всех четырёх вкладках кнопка включается, только когда есть что записывать, и показывает счётчик изменённых полей. Возврат значения к исходному снова её гасит — правки считаются сравнением с состоянием на момент загрузки, а не фактом ввода. Для модов изменением считается и переключённая галочка, и переставленный порядок загрузки, и правка списка карт: состояние сводится к строке и сравнивается со снимком, снятым при открытии вкладки. Co-Authored-By: Claude Opus 5 --- web/static/app.js | 73 +++++++++++++++++++++++++++++++++++++++++++ web/static/index.html | 8 ++--- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/web/static/app.js b/web/static/app.js index 7d15476..6543bb8 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -471,6 +471,7 @@ async function loadServerConfig() { if (!data.exists) { $("#config-empty").textContent = data.hint; + refreshFieldSaveState(SAVE_TARGETS[0]); return; } serverConfigValues = data.values; @@ -479,6 +480,7 @@ async function loadServerConfig() { container.appendChild(buildConfigField(key, data.values[key], CONFIG_HINTS[key])); }); applyFieldFilter("#config-filter", "#config-fields"); + refreshFieldSaveState(SAVE_TARGETS[0]); } // buildConfigField выбирает тип поля по текущему значению: булевы значения @@ -539,12 +541,35 @@ function collectChanged(containerSelector) { return changed; } +// Кнопка «Сохранить» включается, только когда есть что сохранять: на пустой +// вкладке (файл ещё не создан игрой) или без правок нажимать её незачем. +const SAVE_TARGETS = [ + { fields: "#config-fields", button: "#btn-config-save" }, + { fields: "#sandbox-fields", button: "#btn-sandbox-save" }, +]; + +function refreshFieldSaveState(target) { + const count = Object.keys(collectChanged(target.fields)).length; + const button = $(target.button); + button.disabled = count === 0; + button.textContent = count ? `Сохранить (${count})` : "Сохранить"; + button.title = count ? `Изменено параметров: ${count}` : "Изменений нет"; +} + +SAVE_TARGETS.forEach((target) => { + // Слушаем на контейнере: поля пересоздаются при каждой загрузке вкладки. + ["input", "change"].forEach((event) => + $(target.fields).addEventListener(event, () => refreshFieldSaveState(target))); +}); + function markSaved(containerSelector) { $$(`${containerSelector} .cfg-field`).forEach((field) => { const input = field.querySelector("input, select"); if (input) input.dataset.original = input.value; field.classList.remove("changed"); }); + const target = SAVE_TARGETS.find((t) => t.fields === containerSelector); + if (target) refreshFieldSaveState(target); } $("#btn-config-save").addEventListener("click", async () => { @@ -584,6 +609,7 @@ async function loadSandbox() { if (!data.exists) { $("#sandbox-empty").textContent = data.hint; + refreshFieldSaveState(SAVE_TARGETS[1]); return; } const container = $("#sandbox-fields"); @@ -591,6 +617,7 @@ async function loadSandbox() { container.appendChild(buildConfigField(entry.path, entry.value, null)); }); applyFieldFilter("#sandbox-filter", "#sandbox-fields"); + refreshFieldSaveState(SAVE_TARGETS[1]); } $("#btn-sandbox-save").addEventListener("click", async () => { @@ -616,6 +643,10 @@ $("#btn-sandbox-save").addEventListener("click", async () => { // WorkshopItems — что скачать из мастерской, Mods — что включить и в каком // порядке. Панель показывает их одним списком: строка на мод, галочка = // присутствие в Mods, порядок строк = порядок в Mods. +// Снимок списков на момент загрузки: по нему видно, есть ли несохранённые +// правки. Пустая строка означает, что конфига ещё нет и сохранять нечего. +let modsSnapshot = ""; + let mods = { workshop: [], // WorkshopItems enabled: [], // Mods, порядок важен @@ -630,6 +661,8 @@ async function loadMods() { $("#mods-body").hidden = !data.exists; if (!data.exists) { $("#mods-empty").textContent = data.hint; + modsSnapshot = ""; + refreshModsSaveState(); return; } @@ -641,6 +674,7 @@ async function loadMods() { details: mods.details, }; $("#map-list").value = mods.maps.join("\n"); + modsSnapshot = modsState(); renderMods(); lookupWorkshopTitles().catch(() => { /* без интернета останутся идентификаторы — это не ошибка панели */ @@ -711,6 +745,23 @@ function buildModRows() { return rows; } +// modsState сводит списки к строке — так проще всего заметить и переключённую +// галочку, и переставленный порядок загрузки. +function modsState() { + return JSON.stringify({ + enabled: mods.enabled, + workshop: mods.workshop, + maps: $("#map-list").value.split("\n").map((line) => line.trim()).filter(Boolean), + }); +} + +function refreshModsSaveState() { + const button = $("#btn-mods-save"); + const changed = modsSnapshot !== "" && modsState() !== modsSnapshot; + button.disabled = !changed; + button.title = changed ? "Есть несохранённые изменения" : "Изменений нет"; +} + function renderMods() { const filter = $("#mod-filter").value.trim().toLowerCase(); const onlyEnabled = $("#mod-only-enabled").checked; @@ -725,6 +776,8 @@ function renderMods() { $("#mod-summary").textContent = `Модов: ${rows.length}, включено: ${mods.enabled.length}, пакетов мастерской: ${mods.workshop.length}`; + refreshModsSaveState(); + $("#mod-list").innerHTML = visible .map((row) => { const on = mods.enabled.includes(row.modID); @@ -857,6 +910,7 @@ $("#mod-list").addEventListener("drop", (event) => { renderMods(); }); +$("#map-list").addEventListener("input", refreshModsSaveState); $("#mod-filter").addEventListener("input", renderMods); $("#mod-only-enabled").addEventListener("change", renderMods); @@ -921,6 +975,8 @@ $("#btn-mods-save").addEventListener("click", async () => { workshop_items: mods.workshop, map: maps, }); + modsSnapshot = modsState(); + refreshModsSaveState(); ok("Список модов сохранён. Перезапустите сервер, чтобы он их подтянул."); } catch (error) { fail(error); @@ -1302,6 +1358,7 @@ async function loadSettings() { } else { input.value = config[key] ?? ""; } + input.dataset.original = type === "checkbox" ? String(input.checked) : input.value; wrapper.appendChild(input); if (hint) { @@ -1318,6 +1375,7 @@ async function loadSettings() { pathNote.textContent = `Файл конфигурации: ${config.config_path}`; form.appendChild(pathNote); + refreshSettingsSaveState(); await loadUsers(); // Проверка обновления ходит в интернет и может не ответить — она не должна // мешать показу остальных настроек. @@ -1326,6 +1384,21 @@ async function loadSettings() { }); } +function refreshSettingsSaveState() { + const changed = $$("#settings-form input").filter((input) => { + const current = input.type === "checkbox" ? String(input.checked) : input.value; + return current !== input.dataset.original; + }).length; + + const button = $("#btn-settings-save"); + button.disabled = changed === 0; + button.textContent = changed ? `Сохранить настройки (${changed})` : "Сохранить настройки"; + button.title = changed ? `Изменено полей: ${changed}` : "Изменений нет"; +} + +["input", "change"].forEach((event) => + $("#settings-form").addEventListener(event, refreshSettingsSaveState)); + $("#btn-settings-save").addEventListener("click", async () => { const payload = {}; SETTINGS_FIELDS.forEach(([key, , type]) => { diff --git a/web/static/index.html b/web/static/index.html index a34c709..380aaa1 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -145,7 +145,7 @@
- +
@@ -161,7 +161,7 @@
- +
@@ -176,7 +176,7 @@

Галочка включает мод, перетаскивание задаёт порядок загрузки. Сервер сам скачивает моды из мастерской при запуске.

- + @@ -290,7 +290,7 @@

Настройки менеджера

- +

Пользователи панели