import { listZipFiles } from '../zip/list_zip_files.js'; import { addFileWithDedup } from './add_file_with_dedup.js'; import { render } from './render.js'; /** * Обрабатывает список файлов из обычного file input. * * @param {{fileKeys: Set, nodes: Array, busy: boolean}} state Состояние picker. * @param {{allowedExt: string[]}} cfg Конфигурация разрешённых расширений. * @param {FileList|File[]} files Выбранные browser File objects. * @param {{tableBodyEl: HTMLElement, countEl: HTMLElement}} elements DOM-элементы вывода. * @returns {Promise} Завершается после разбора всех ZIP и перерисовки таблицы. * * Обычные документы добавляются сразу. ZIP читаются асинхронно в браузере; * ошибка одного архива не отменяет обработку остальных выбранных файлов. */ export async function addFiles(state, cfg, files, elements) { for (const file of Array.from(files)) { if (!file.name.toLowerCase().endsWith('.zip')) { // Фильтр повторяет accept в HTML, потому что accept не является защитой API. if (!cfg.allowedExt.some((extension) => file.name.toLowerCase().endsWith(extension.toLowerCase()))) { continue; } // Обычный файл получает имя как путь: у file input нет относительного пути. addFileWithDedup(state, { id: crypto.randomUUID(), kind: 'file', name: file.name, path: file.name, file, children: [], expanded: true, }); continue; } try { // listZipFiles возвращает дерево только с разрешёнными leaf-файлами. const zipTree = await listZipFiles(file, cfg.allowedExt, cfg.limits); if (zipTree) addFileWithDedup(state, zipTree); } catch (error) { // Битый или небезопасный ZIP пропускается, чтобы не блокировать picker. if (typeof cfg.onError === 'function') { cfg.onError(error, file); } else if (elements.statusEl) { elements.statusEl.textContent = `Ошибка чтения архива ${file.name}: ${error.message}`; } continue; } } // Единая перерисовка после всей пачки предотвращает промежуточные состояния UI. render(state, elements, cfg); } /** * Создаёт обработчик change для обычного выбора файлов. * * @param {object} state Состояние picker, включая флаг busy. * @param {object} cfg Конфигурация picker. * @param {object} elements DOM-элементы picker. * @returns {Function} Async-обработчик для addEventListener('change', ...). * * busy устанавливается синхронно до первого await. Поэтому второе быстрое * событие выбора не запускает параллельный разбор и не меняет дерево конкурирующе. */ export function onFilesChange(state, cfg, elements) { return async () => { if (state.busy) return; state.busy = true; try { await addFiles(state, cfg, elements.fileInputEl.files, elements); } finally { // Даже исключение вне внутреннего ZIP-catch не оставляет picker заблокированным. elements.fileInputEl.value = ''; state.busy = false; } }; }