Files
photo-editor/public/js/upload.js
Coder 401969cfdb
Some checks failed
test / test (push) Failing after 24s
v2: модульный монолит — очередь, персистентность, безопасность, SSE, тесты
Реализация целевого дизайна (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/
2026-08-27 12:52:27 +07:00

124 lines
3.3 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}
});
}
}