Some checks failed
test / test (push) Failing after 24s
Реализация целевого дизайна (docs/architecture/target-design.md): - src/: модульная структура (config, logger, errors, http, core: auth/history/jobs/engines/comfy/codex/images) - Очередь заданий: FIFO, кониурентность codex=1 / comfy-upscale=2, MAX_QUEUED_JOBS=5, JSONL-журнал и восстановление после рестарта (running → interrupted, upscale requeue) - Персистентные сессии (sha256-хеши токенов), scrypt-хеш пароля, rate-limit входа, Origin-проверка, magic-byte валидация аплоадов - Атомарная запись манифеста истории, каскадное удаление, URL с фактическим расширением + 302-алиас /image.jpg, миграция history/ → data/history - Даунскейл sharp до 1024px перед Codex + масштабирование области референса - SSE /api/events с фолбэком на polling, фронтенд переведён на ES-модули (public/js/) - Тесты node:test + supertest (27), CI workflow test.yml, Docker/деплой: том kadr-data - Документация: README, docs/architecture/
124 lines
3.3 KiB
JavaScript
124 lines
3.3 KiB
JavaScript
/**
|
||
* upload.js — Загрузка и импорт фотографий через drag-and-drop или file picker
|
||
*/
|
||
|
||
import { apiFetch } from "./api.js";
|
||
import { getState, setBusy } from "./state.js";
|
||
import { setJobStatus, isHeicFile, ACCEPTED_TYPES, MAX_FILE_SIZE } from "./ui.js";
|
||
import { refreshHistory } from "./history-tree.js";
|
||
|
||
export async function importPhoto(file) {
|
||
const form = new FormData();
|
||
form.append("photo", file, file.name);
|
||
|
||
const data = await apiFetch("/api/history/import", {
|
||
method: "POST",
|
||
body: form,
|
||
cache: "no-store",
|
||
});
|
||
|
||
return data.version;
|
||
}
|
||
|
||
export async function handleFiles(fileList) {
|
||
const files = Array.from(fileList || []).filter((file) => {
|
||
const fileType = String(file.type || "").toLowerCase();
|
||
|
||
if (!ACCEPTED_TYPES.has(fileType) && !isHeicFile(file)) {
|
||
setJobStatus("error", "Поддерживаются только фотографии JPEG, PNG, WebP или HEIC.");
|
||
return false;
|
||
}
|
||
|
||
if (file.size > MAX_FILE_SIZE) {
|
||
setJobStatus("error", "Файл слишком большой. Максимальный размер — 25 МБ.");
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
});
|
||
|
||
if (files.length === 0) return;
|
||
|
||
setBusy(true);
|
||
|
||
for (let i = 0; i < files.length; i++) {
|
||
const file = files[i];
|
||
|
||
setJobStatus(
|
||
"progress",
|
||
files.length > 1
|
||
? `Загрузка ${i + 1} из ${files.length}…`
|
||
: "Загружаем фотографию в историю…"
|
||
);
|
||
|
||
try {
|
||
await importPhoto(file);
|
||
} catch (error) {
|
||
setBusy(false);
|
||
setJobStatus(
|
||
"error",
|
||
error instanceof Error ? error.message : "Не удалось загрузить фотографию."
|
||
);
|
||
return;
|
||
}
|
||
}
|
||
|
||
await refreshHistory(true);
|
||
setJobStatus("idle", "");
|
||
setBusy(false);
|
||
}
|
||
|
||
export function initUpload() {
|
||
const dropZone = document.querySelector("#drop-zone");
|
||
const photoInput = document.querySelector("#photo-input");
|
||
|
||
if (dropZone && photoInput) {
|
||
dropZone.addEventListener("click", () => {
|
||
const state = getState();
|
||
if (!state.isBusy) {
|
||
photoInput.click();
|
||
}
|
||
});
|
||
|
||
dropZone.addEventListener("keydown", (event) => {
|
||
if (event.key === "Enter" || event.key === " ") {
|
||
event.preventDefault();
|
||
const state = getState();
|
||
if (!state.isBusy) {
|
||
photoInput.click();
|
||
}
|
||
}
|
||
});
|
||
|
||
photoInput.addEventListener("change", (event) => {
|
||
handleFiles(event.target.files);
|
||
photoInput.value = "";
|
||
});
|
||
|
||
const preventDefaults = (e) => {
|
||
e.preventDefault();
|
||
e.stopPropagation();
|
||
};
|
||
|
||
["dragenter", "dragover", "dragleave", "drop"].forEach((evt) => {
|
||
dropZone.addEventListener(evt, preventDefaults, false);
|
||
document.body.addEventListener(evt, preventDefaults, false);
|
||
});
|
||
|
||
["dragenter", "dragover"].forEach((evt) => {
|
||
dropZone.addEventListener(evt, () => dropZone.classList.add("dragover"), false);
|
||
});
|
||
|
||
["dragleave", "drop"].forEach((evt) => {
|
||
dropZone.addEventListener(evt, () => dropZone.classList.remove("dragover"), false);
|
||
});
|
||
|
||
dropZone.addEventListener("drop", (event) => {
|
||
const state = getState();
|
||
if (!state.isBusy && event.dataTransfer?.files) {
|
||
handleFiles(event.dataTransfer.files);
|
||
}
|
||
});
|
||
}
|
||
}
|