Initial deployable Flask application

This commit is contained in:
Repinoid
2026-10-21 12:00:44 +03:00
commit 2450c0c9a1
48 changed files with 6951 additions and 0 deletions
+84
View File
@@ -0,0 +1,84 @@
import { listZipFiles } from '../zip/list_zip_files.js';
import { addFileWithDedup } from './add_file_with_dedup.js';
import { rebaseTree } from './rebase_tree.js';
import { render } from './render.js';
/**
* Создаёт async-обработчик выбора каталога через webkitdirectory.
*
* @param {{nodes: Array, fileKeys: Set<string>, busy: boolean}} state Состояние picker.
* @param {{allowedExt: string[]}} cfg Разрешённые расширения.
* @param {{folderInputEl: HTMLInputElement, tableBodyEl: HTMLElement, countEl: HTMLElement}} elements DOM-элементы.
* @returns {Function} Async-обработчик события change.
*
* Браузер отдаёт плоский FileList с webkitRelativePath. Обработчик группирует
* его по первой части пути, создаёт промежуточные папки и затем добавляет каждый
* корень через общую дедупликацию. ZIP внутри каталога раскрывается тем же кодом,
* что и ZIP из обычного file input.
*/
export function onFolderChange(state, cfg, elements) {
return async () => {
if (state.busy) return;
state.busy = true;
try {
const roots = new Map();
for (const file of Array.from(elements.folderInputEl.files)) {
// Для webkitdirectory путь начинается с выбранной корневой папки.
const parts = (file.webkitRelativePath || file.name).split('/');
const relativePath = parts.slice(1).join('/') || file.name;
const lowerPath = relativePath.toLowerCase();
const rootName = parts[0] || file.name;
if (!roots.has(rootName)) {
// Один root на выбранный каталог позволяет сохранить дерево целиком.
roots.set(rootName, { id: crypto.randomUUID(), kind: 'folder', name: rootName,
path: rootName, children: [], expanded: false });
}
const root = roots.get(rootName);
// Вставляет узел по его пути, создавая отсутствующие промежуточные папки.
const addToFolder = (node) => {
let current = root;
const nodeParts = node.path.split('/').slice(1);
nodeParts.forEach((part, index) => {
const last = index === nodeParts.length - 1;
let child = current.children.find((item) => item.name === part);
if (!child) {
child = last ? node : { id: crypto.randomUUID(), kind: 'folder', name: part,
path: `${rootName}/${nodeParts.slice(0, index + 1).join('/')}`,
children: [], expanded: false };
current.children.push(child);
}
current = child;
});
};
if (lowerPath.endsWith('.zip')) {
try {
// ZIP rebased на имя выбранной папки перед вставкой в общий root.
const zip = await listZipFiles(file, cfg.allowedExt, cfg.limits);
if (zip) {
rebaseTree(zip, rootName);
addToFolder(zip);
}
} catch (error) {
// Ошибка одного ZIP не должна терять остальные файлы каталога.
if (typeof cfg.onError === 'function') {
cfg.onError(error, file);
} else if (elements.statusEl) {
elements.statusEl.textContent = `Ошибка чтения архива ${file.name}: ${error.message}`;
}
continue;
}
} else if (cfg.allowedExt.some((extension) => lowerPath.endsWith(extension))) {
addToFolder({ id: crypto.randomUUID(), kind: 'file', name: parts.at(-1),
path: `${rootName}/${relativePath}`, file, children: [], expanded: true });
}
}
// Дедупликация выполняется после сборки каждого корня каталога.
roots.forEach((root) => addFileWithDedup(state, root));
// Сброс value позволяет выбрать тот же каталог повторно.
elements.folderInputEl.value = '';
render(state, elements, cfg);
} finally {
state.busy = false;
}
};
}