Импорт пресетов сборок из ZIP-пакета
Крупные сборки раздают готовый комплект настроек архивом. Панель разбирает его сама: находит серверный .ini, песочницу и точки появления, показывает, что нашла (сколько модов, карт, параметров), и только по подтверждению раскладывает файлы в новый или существующий профиль. Порты, пароли и название сервера при этом можно оставить свои — они относятся к машине, а не к сборке; у нового профиля они берутся из активного. Прежние конфиги остаются рядом как .bak, мир не трогается, а импорт в работающий профиль отклоняется: PZ перезаписал бы файлы своими при остановке. Посторонние .ini из архива (настройки модов) за конфиг сервера не принимаются: он опознаётся по знакомым параметрам. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1562,6 +1562,159 @@ $("#profile-add-form").addEventListener("submit", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Импорт пресета сборки
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Сборки модов раздают готовый комплект настроек одним архивом. Панель сначала
|
||||
// показывает, что в нём нашла, и только потом что-то пишет: пресет заменяет
|
||||
// конфиг профиля целиком, и делать это вслепую нельзя.
|
||||
let presetSets = [];
|
||||
|
||||
// Файл отправляется дважды — на разбор и на импорт. Держать его на сервере
|
||||
// между запросами не за чем: он уже лежит в браузере.
|
||||
async function postFile(path, form) {
|
||||
const response = await fetch(path, {
|
||||
method: "POST",
|
||||
headers: { [CSRF_HEADER]: "1" },
|
||||
credentials: "same-origin",
|
||||
body: form,
|
||||
});
|
||||
if (response.status === 401) {
|
||||
showAuth(false);
|
||||
throw new Error(t("auth.expired"));
|
||||
}
|
||||
const text = await response.text();
|
||||
const data = text ? JSON.parse(text) : {};
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || t("error.http", { status: response.status }));
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function presetFile() {
|
||||
const input = $("#preset-file");
|
||||
return input.files && input.files[0] ? input.files[0] : null;
|
||||
}
|
||||
|
||||
$("#preset-file").addEventListener("change", async () => {
|
||||
const file = presetFile();
|
||||
$("#preset-details").hidden = true;
|
||||
if (!file) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
toast(t("import.reading"));
|
||||
try {
|
||||
const data = await postFile("/api/presets/preview", form);
|
||||
presetSets = data.presets || [];
|
||||
renderPresetSets();
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
}
|
||||
});
|
||||
|
||||
function renderPresetSets() {
|
||||
if (!presetSets.length) return;
|
||||
|
||||
const picker = $("#preset-set");
|
||||
picker.innerHTML = presetSets
|
||||
.map((set) => `<option value="${escapeHTML(set.name)}">${escapeHTML(set.name)}</option>`)
|
||||
.join("");
|
||||
// Выбор нужен, только когда в архиве несколько наборов.
|
||||
$("#preset-pick").hidden = presetSets.length < 2;
|
||||
|
||||
$("#preset-details").hidden = false;
|
||||
renderPresetSummary();
|
||||
}
|
||||
|
||||
function currentPreset() {
|
||||
const name = $("#preset-set").value;
|
||||
return presetSets.find((set) => set.name === name) || presetSets[0];
|
||||
}
|
||||
|
||||
function renderPresetSummary() {
|
||||
const set = currentPreset();
|
||||
if (!set) return;
|
||||
|
||||
const lines = [];
|
||||
if (set.server_name) lines.push(t("import.server", { name: set.server_name }));
|
||||
if (set.has_ini) {
|
||||
lines.push(t("import.mods", { mods: set.mods, workshop: set.workshop_items }));
|
||||
if ((set.maps || []).length) lines.push(t("import.maps", { maps: set.maps.join(", ") }));
|
||||
} else {
|
||||
lines.push(t("import.noINI"));
|
||||
}
|
||||
lines.push(t("import.counts", { settings: set.settings, sandbox: set.sandbox_settings }));
|
||||
lines.push(t(set.has_spawn_regions ? "import.spawnYes" : "import.spawnNo"));
|
||||
|
||||
$("#preset-summary").replaceChildren(...lines.map((line) => {
|
||||
const el = document.createElement("div");
|
||||
el.textContent = line;
|
||||
return el;
|
||||
}));
|
||||
|
||||
// Идентификатор профиля предлагаем по имени файлов из архива: обычно оно и
|
||||
// называет сборку.
|
||||
$("#preset-id").value = set.name.replace(/[^A-Za-z0-9_-]/g, "-").slice(0, 32);
|
||||
$("#preset-profile").innerHTML = profiles.list
|
||||
.map((p) => `<option value="${escapeHTML(p.id)}">${escapeHTML(p.title || p.id)}</option>`)
|
||||
.join("");
|
||||
$("#preset-profile").value = profiles.active;
|
||||
}
|
||||
|
||||
$("#preset-set").addEventListener("change", renderPresetSummary);
|
||||
|
||||
$("#preset-target").addEventListener("change", () => {
|
||||
const existing = $("#preset-target").value === "existing";
|
||||
$("#preset-new-fields").hidden = existing;
|
||||
$("#preset-existing-fields").hidden = !existing;
|
||||
});
|
||||
|
||||
$("#btn-preset-import").addEventListener("click", async () => {
|
||||
const file = presetFile();
|
||||
const set = currentPreset();
|
||||
if (!file || !set) {
|
||||
toast(t("import.noFile"), "err");
|
||||
return;
|
||||
}
|
||||
|
||||
const existing = $("#preset-target").value === "existing";
|
||||
const profile = existing ? $("#preset-profile").value : $("#preset-id").value.trim();
|
||||
if (!confirm(t("import.confirm", { name: set.name, profile }))) return;
|
||||
|
||||
const form = new FormData();
|
||||
form.append("file", file);
|
||||
form.append("preset", set.name);
|
||||
form.append("target", existing ? "existing" : "new");
|
||||
form.append("profile", profile);
|
||||
form.append("title", set.server_name || set.name);
|
||||
form.append("keep", $$("#preset-details [data-keep]")
|
||||
.filter((box) => box.checked)
|
||||
.map((box) => box.dataset.keep)
|
||||
.join(","));
|
||||
|
||||
const button = $("#btn-preset-import");
|
||||
button.disabled = true;
|
||||
try {
|
||||
const result = await postFile("/api/presets/import", form);
|
||||
ok(t("import.done", { profile: result.profile, files: (result.files || []).join(", ") }));
|
||||
if ((result.kept || []).length) {
|
||||
toast(t("import.kept", { profile: result.kept_from, keys: result.kept.join(", ") }));
|
||||
}
|
||||
$("#preset-file").value = "";
|
||||
$("#preset-details").hidden = true;
|
||||
await loadProfiles();
|
||||
// Импортированный профиль мог оказаться активным — вкладки показывают
|
||||
// именно его конфиг.
|
||||
loadTab(currentTab);
|
||||
} catch (error) {
|
||||
fail(error);
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Бэкапы
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user