80 lines
4.2 KiB
JavaScript
80 lines
4.2 KiB
JavaScript
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: true });
|
||
}
|
||
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: true };
|
||
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);
|
||
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;
|
||
}
|
||
};
|
||
} |