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/
139 lines
4.0 KiB
JavaScript
139 lines
4.0 KiB
JavaScript
/**
|
|
* ui.js — Утилиты форматирования, отображения статусов и DOM-элементов
|
|
*/
|
|
|
|
export const MAX_FILE_SIZE = 25 * 1024 * 1024;
|
|
export const ACCEPTED_TYPES = new Set([
|
|
"image/jpeg",
|
|
"image/jpg",
|
|
"image/png",
|
|
"image/webp",
|
|
"image/heic",
|
|
"image/heif",
|
|
]);
|
|
|
|
export function isHeicFile(file) {
|
|
const type = String(file?.type || "").toLowerCase();
|
|
const name = String(file?.name || "").toLowerCase();
|
|
return (
|
|
type === "image/heic" ||
|
|
type === "image/heif" ||
|
|
name.endsWith(".heic") ||
|
|
name.endsWith(".heif")
|
|
);
|
|
}
|
|
|
|
export function formatFileSize(bytes) {
|
|
if (!bytes || bytes <= 0) return "";
|
|
const units = ["Б", "КБ", "МБ", "ГБ"];
|
|
let value = bytes;
|
|
let unitIndex = 0;
|
|
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
value /= 1024;
|
|
unitIndex++;
|
|
}
|
|
return `${value.toFixed(value >= 10 || unitIndex === 0 ? 0 : 1)} ${units[unitIndex]}`;
|
|
}
|
|
|
|
export function versionFileName(version) {
|
|
const dateStr = version?.createdAt
|
|
? new Date(version.createdAt).toISOString().replace(/[:.]/g, "-").slice(0, 19)
|
|
: "photo";
|
|
const ext = version?.ext || "jpg";
|
|
const label = version?.label ? version.label.replace(/[^\p{L}\p{N}._-]+/gu, "-") : "version";
|
|
return `kadr-${label}-${dateStr}.${ext}`;
|
|
}
|
|
|
|
export function formatHistoryDate(iso) {
|
|
if (!iso) return "";
|
|
try {
|
|
const d = new Date(iso);
|
|
const date = d.toLocaleDateString("ru-RU", {
|
|
day: "numeric",
|
|
month: "short",
|
|
});
|
|
const time = d.toLocaleTimeString("ru-RU", {
|
|
hour: "2-digit",
|
|
minute: "2-digit",
|
|
});
|
|
return `${date}, ${time}`;
|
|
} catch {
|
|
return iso;
|
|
}
|
|
}
|
|
|
|
export function setJobStatus(kind, message) {
|
|
const jobStatus = document.querySelector("#job-status");
|
|
const jobStatusText = document.querySelector("#job-status-text");
|
|
|
|
if (!jobStatus || !jobStatusText) return;
|
|
|
|
if (kind === "idle") {
|
|
jobStatus.hidden = true;
|
|
jobStatus.classList.remove("status-error");
|
|
jobStatusText.textContent = "";
|
|
return;
|
|
}
|
|
|
|
jobStatus.hidden = false;
|
|
jobStatus.classList.toggle("status-error", kind === "error");
|
|
jobStatusText.textContent = message;
|
|
}
|
|
|
|
export function resetResult() {
|
|
const resultImage = document.querySelector("#result-image");
|
|
const resultPlaceholder = document.querySelector("#result-placeholder");
|
|
const resultLabel = document.querySelector("#result-label");
|
|
const downloadLink = document.querySelector("#download-link");
|
|
|
|
if (resultImage) {
|
|
resultImage.hidden = true;
|
|
resultImage.removeAttribute("src");
|
|
}
|
|
if (resultPlaceholder) resultPlaceholder.hidden = false;
|
|
if (resultLabel) resultLabel.textContent = "";
|
|
if (downloadLink) downloadLink.hidden = true;
|
|
}
|
|
|
|
export function clearSourcePreview() {
|
|
const sourceImage = document.querySelector("#source-image");
|
|
const sourcePlaceholder = document.querySelector("#source-placeholder");
|
|
const downloadCurrent = document.querySelector("#download-current");
|
|
|
|
if (sourceImage) {
|
|
sourceImage.hidden = true;
|
|
sourceImage.removeAttribute("src");
|
|
}
|
|
if (sourcePlaceholder) sourcePlaceholder.hidden = false;
|
|
if (downloadCurrent) downloadCurrent.hidden = true;
|
|
}
|
|
|
|
export function showSourcePreview(url, title, meta) {
|
|
const sourceImage = document.querySelector("#source-image");
|
|
const sourcePlaceholder = document.querySelector("#source-placeholder");
|
|
const dropTitle = document.querySelector("#drop-title");
|
|
const fileMeta = document.querySelector("#file-meta");
|
|
|
|
if (sourceImage) {
|
|
sourceImage.src = url;
|
|
sourceImage.hidden = false;
|
|
}
|
|
if (sourcePlaceholder) sourcePlaceholder.hidden = true;
|
|
if (dropTitle && title) dropTitle.textContent = title;
|
|
if (fileMeta && meta) fileMeta.textContent = meta;
|
|
}
|
|
|
|
export function updateDownloadCurrent(version) {
|
|
const downloadCurrent = document.querySelector("#download-current");
|
|
if (!downloadCurrent) return;
|
|
|
|
if (!version || !version.url) {
|
|
downloadCurrent.hidden = true;
|
|
return;
|
|
}
|
|
|
|
downloadCurrent.href = version.url;
|
|
downloadCurrent.download = versionFileName(version);
|
|
downloadCurrent.hidden = false;
|
|
}
|