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, 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; } }; }