Кнопка «Сохранить» активна только при несохранённых изменениях
Раньше она предлагала сохранить даже пустую вкладку: на свежей установке конфигов ещё нет, полей на странице ноль, а кнопка зовёт нажать себя. Теперь на всех четырёх вкладках кнопка включается, только когда есть что записывать, и показывает счётчик изменённых полей. Возврат значения к исходному снова её гасит — правки считаются сравнением с состоянием на момент загрузки, а не фактом ввода. Для модов изменением считается и переключённая галочка, и переставленный порядок загрузки, и правка списка карт: состояние сводится к строке и сравнивается со снимком, снятым при открытии вкладки. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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]) => {
|
||||
|
||||
Reference in New Issue
Block a user