/** * Folder Tree Module * Handles folder tree rendering and multi-select */ (function() { 'use strict'; // ── Classify & Copy helpers ──────────────────────────────────────────── function classifyOn() { var c = window.app.modules.classify; return c && c.isEnabled(); } // All file objects in a folder's (already-scanned) subtree — group-drag. function subtreeFiles(folder, out) { out = out || []; (folder.files || []).forEach(function (f) { out.push(f); }); (folder.children || []).forEach(function (c) { subtreeFiles(c, out); }); return out; } function keysFor(files) { var c = window.app.modules.classify; return files.map(function (f) { return c.srcKeyForFile(f); }); } // A small status dot reflecting a file's classification state. var STATE_TITLE = { none: 'unassigned', tracking: 'has tracking number, needs a transmittal', transmittal: 'in a transmittal, needs a tracking number', partial: 'placed, but the name is incomplete', done: 'fully classified', excluded: 'excluded — will not be copied', }; function stateDot(state) { var dot = document.createElement('span'); dot.className = 'cl-dot cl-dot--' + state; dot.title = STATE_TITLE[state] || ''; return dot; } // ── Classify-mode source-tree filters ────────────────────────────────── // The goal in either target tab is to assign-or-exclude every file. Each // file falls in one bucket FOR THE ACTIVE AXIS — unassigned / assigned / // excluded — and three "Show …" toggles control which buckets are visible // (so unchecking Assigned+Excluded leaves only what's left to do). A folder // whose whole scanned subtree is filtered away is itself hidden. var showFilters = { unassigned: true, assigned: true, excluded: true }; function setShowFilters(f) { showFilters = { unassigned: f.unassigned !== false, assigned: f.assigned !== false, excluded: f.excluded !== false, }; render(); } function allFiltersOn() { return showFilters.unassigned && showFilters.assigned && showFilters.excluded; } function activeAxis() { var tt = window.app.modules.targetTree; return (tt && tt.activeAxis) ? tt.activeAxis() : 'tracking'; } // Bucket a file relative to the active axis: 'excluded' | 'assigned' | 'unassigned'. function fileCategory(file) { var c = window.app.modules.classify; var a = c.getAssignment(c.srcKeyForFile(file)); if (a && a.excluded) return 'excluded'; var assigned = a && (activeAxis() === 'transmittal' ? a.transmittalNodeId : a.trackingNodeId); return assigned ? 'assigned' : 'unassigned'; } function fileVisible(file) { return !!showFilters[fileCategory(file)]; } function subtreeVisibleCount(folder) { var n = 0; subtreeFiles(folder).forEach(function (f) { if (fileVisible(f)) n++; }); return n; } // Hide a folder only when it's fully scanned (so we never hide one that may // still reveal files) and the active filters leave nothing visible in it. function folderHidden(folder) { if (!classifyOn() || allFiltersOn()) return false; if (folder.scanState && folder.scanState !== 'done') return false; return subtreeVisibleCount(folder) === 0; } // All scanned files (for the per-bucket counts on the filter checkboxes). function allClassifyFiles() { var out = []; (window.app.folderTree || []).forEach(function (f) { subtreeFiles(f, out); }); return out; } function updateFilterCounts() { if (!classifyOn()) return; var n = { unassigned: 0, assigned: 0, excluded: 0 }; allClassifyFiles().forEach(function (f) { n[fileCategory(f)]++; }); ['unassigned', 'assigned', 'excluded'].forEach(function (k) { var el = document.getElementById('show' + k.charAt(0).toUpperCase() + k.slice(1) + 'Count'); if (el) el.textContent = '(' + n[k] + ')'; }); } /** * Render the folder tree */ function render() { const container = window.app.dom.folderTree; wireClassifyInteractions(); container.innerHTML = ''; updateFilterCounts(); if (window.app.folderTree.length === 0) { container.innerHTML = '
No folders found
'; return; } window.app.folderTree.forEach(folder => { if (folderHidden(folder)) return; const element = createFolderElement(folder); container.appendChild(element); }); if (classifyOn() && !container.children.length) { container.innerHTML = '
Nothing matches the current filters.
'; } updateSelectedCount(); } /** * Populate a folder row's count element with "direct+total" counts, e.g. * "(2+10 folders, 15+300 files)" — direct (immediate children) shows as * soon as the folder's own directory is read; the total (whole subtree) * grows and flashes grey until the subtree is fully scanned, then goes * solid. The "+total" part is omitted once scanning is done and there's * nothing deeper (direct == total). */ function populateCount(el, folder) { el.textContent = ''; el.classList.remove('done'); const st = folder.scanState; if (st === 'pending') return; if (st === 'zip-pending') { el.textContent = '(zip — open to scan)'; return; } if (st === 'scanning') { el.textContent = 'scanning…'; return; } const done = st === 'done'; // When fully scanned both numbers are blue; .done turns the labels blue too. if (done) el.classList.add('done'); const dDir = folder.subdirCount || 0, tDir = folder.runDirs || 0; const dFile = folder.fileCount || 0, tFile = folder.runFiles || 0; const frag = document.createDocumentFragment(); frag.appendChild(document.createTextNode('(')); if (dDir > 0 || tDir > 0) { appendPair(frag, dDir, tDir, done); appendLabel(frag, tDir === 1 ? ' folder, ' : ' folders, '); } appendPair(frag, dFile, tFile, done); appendLabel(frag, tFile === 1 ? ' file)' : ' files)'); el.appendChild(frag); } // The "folders"/"files" word labels — blue only once the row is .done. function appendLabel(frag, text) { const s = document.createElement('span'); s.className = 'ct-label'; s.textContent = text; frag.appendChild(s); } // Append "" (always a completed/blue number) and, when there's a // subtree (or scanning is ongoing), "+" with the total in a span // that greys + pulses until final, then turns blue. function appendPair(frag, direct, total, done) { const d = document.createElement('span'); d.className = 'ct-direct'; d.textContent = String(direct); frag.appendChild(d); if (!done || total > direct) { frag.appendChild(document.createTextNode('+')); const t = document.createElement('span'); t.className = 'ct-total' + (done ? '' : ' pending'); t.textContent = String(total); frag.appendChild(t); } } /** * Create a folder element */ function createFolderElement(folder, level = 0) { const div = document.createElement('div'); const item = document.createElement('div'); item.className = 'folder-item'; // Grey the row until its subtree is fully scanned (scanState 'done'); // 'scanning' rows also get a subtle pulse via CSS. if (folder.scanState && folder.scanState !== 'done') { item.classList.add('scanning'); } item.dataset.path = folder.path; item.style.paddingLeft = `${level * 1.5}rem`; // Check if selected if (window.app.selectedFolders.has(folder.path)) { item.classList.add('selected'); } // Classify mode: the folder row is a drag source for a group-drag of // every file in its subtree. if (classifyOn()) { item.draggable = true; item.addEventListener('dragstart', function (e) { e.stopPropagation(); var files = subtreeFiles(folder); if (!files.length) { e.preventDefault(); return; } window.app.modules.dnd.setDrag(keysFor(files), e); }); } // Toggle button: shown when the folder has children OR hasn't been // scanned yet (it might have children — expanding triggers its scan). const toggle = document.createElement('span'); toggle.className = 'folder-toggle'; const mightHaveChildren = (folder.children && folder.children.length > 0) || folder.scanState === 'pending' || folder.scanState === 'zip-pending' // Classify mode: a folder with files (even none of subfolders) is // expandable so its files can be revealed and dragged. || (classifyOn() && folder.files && folder.files.length > 0); if (mightHaveChildren) { toggle.textContent = folder.expanded ? '▼' : '▶'; toggle.addEventListener('click', (e) => { e.stopPropagation(); const recursive = e.ctrlKey || e.metaKey; toggleFolder(folder, recursive); }); } else { toggle.textContent = ' '; } item.appendChild(toggle); // Folder icon (different for ZIP files) const icon = document.createElement('span'); icon.className = 'folder-icon'; if (folder.isZipRoot) { icon.innerHTML = '📦'; // 📦 } else if (folder.isVirtualDir) { icon.innerHTML = '📂'; // 📂 } else { icon.innerHTML = '📁'; // 📁 } item.appendChild(icon); // Classify mode: an aggregate state dot for the folder's subtree. if (classifyOn()) { const agg = aggregateState(subtreeFiles(folder)); if (agg) item.appendChild(stateDot(agg)); } // Folder name const name = document.createElement('span'); name.className = 'folder-name'; name.textContent = folder.name; item.appendChild(name); // Subfolder / file counts (immediate). Greyed via the row's .scanning // class until the subtree is fully scanned. const count = document.createElement('span'); count.className = 'folder-count'; populateCount(count, folder); item.appendChild(count); // Extract button for ZIP roots if (folder.isZipRoot) { const extractBtn = document.createElement('button'); extractBtn.className = 'btn btn-sm zip-extract-btn'; extractBtn.textContent = '📤 Extract'; extractBtn.title = 'Extract ZIP contents to folder'; extractBtn.addEventListener('click', async (e) => { e.stopPropagation(); await handleExtractZip(folder); }); item.appendChild(extractBtn); } // Extract All button for folders with ZIP descendants (but not ZIP roots themselves) if (!folder.isZipRoot && !folder.isVirtualDir) { const zipCount = countZipDescendants(folder); if (zipCount > 0) { const extractAllBtn = document.createElement('button'); extractAllBtn.className = 'btn btn-sm zip-extract-all-btn'; extractAllBtn.textContent = `📤 Extract All (${zipCount})`; extractAllBtn.title = `Extract all ${zipCount} ZIP file(s) in this folder`; extractAllBtn.addEventListener('click', async (e) => { e.stopPropagation(); await handleExtractAllZips(folder); }); item.appendChild(extractAllBtn); } } // Click handler for selection item.addEventListener('click', (e) => { handleFolderClick(folder, e); }); div.appendChild(item); // Children (if expanded) if (folder.expanded && folder.children && folder.children.length > 0) { const childrenDiv = document.createElement('div'); childrenDiv.className = 'folder-children'; folder.children.forEach(child => { if (folderHidden(child)) return; const childElement = createFolderElement(child, level + 1); childrenDiv.appendChild(childElement); }); div.appendChild(childrenDiv); } // Classify mode: list this folder's own files (draggable leaves) when // expanded, so they can be dropped onto the target trees. if (classifyOn() && folder.expanded && folder.files && folder.files.length > 0) { const filesDiv = document.createElement('div'); filesDiv.className = 'folder-children folder-files'; folder.files.forEach(function (file) { if (classifyOn() && !fileVisible(file)) return; filesDiv.appendChild(createFileElement(file, level + 1)); }); div.appendChild(filesDiv); } return div; } /** * Create a draggable source-file row (classify mode only). */ function createFileElement(file, level) { const c = window.app.modules.classify; const item = document.createElement('div'); item.className = 'file-item'; item.style.paddingLeft = `${level * 1.5}rem`; item.draggable = true; item.title = 'Click to preview · drag onto a tracking folder or transmittal to assign'; const key = c.srcKeyForFile(file); item.dataset.key = key; const st = c.fileState(file); if (st === 'excluded') item.classList.add('excluded'); item.appendChild(stateDot(st)); const icon = document.createElement('span'); icon.className = 'file-icon'; icon.innerHTML = '📄'; // 📄 item.appendChild(icon); const name = document.createElement('span'); name.className = 'file-name'; name.textContent = zddc.joinExtension(file.originalFilename, file.extension); item.appendChild(name); item.addEventListener('dragstart', function (e) { e.stopPropagation(); window.app.modules.dnd.setDrag([key], e); }); return item; } /** * Handle folder click with multi-select support */ function handleFolderClick(folder, event) { if (event.ctrlKey || event.metaKey) { // Ctrl+Click: Toggle selection if (window.app.selectedFolders.has(folder.path)) { window.app.selectedFolders.delete(folder.path); } else { window.app.selectedFolders.add(folder.path); } } else if (event.shiftKey) { // Shift+Click: Range selection const visibleFolders = getVisibleFolders(); const currentIndex = visibleFolders.findIndex(f => f.path === folder.path); if (currentIndex >= 0 && window.app.lastSelectedFolderPath) { const lastIndex = visibleFolders.findIndex(f => f.path === window.app.lastSelectedFolderPath); if (lastIndex >= 0) { const start = Math.min(currentIndex, lastIndex); const end = Math.max(currentIndex, lastIndex); // Select range for (let i = start; i <= end; i++) { window.app.selectedFolders.add(visibleFolders[i].path); } } } else { window.app.selectedFolders.add(folder.path); } } else { // Normal click: Single selection window.app.selectedFolders.clear(); window.app.selectedFolders.add(folder.path); } // Remember last selected for shift-click window.app.lastSelectedFolderPath = folder.path; // Re-render tree render(); // Load files from selected folders loadFilesFromSelectedFolders(); } /** * Handle ZIP extraction */ async function handleExtractZip(folder) { if (!folder.isZipRoot || !folder.zipPath) return; try { const confirmed = confirm(`Extract "${folder.name}" to a new folder?\n\nThis will create a folder named "${folder.name.replace(/\.zip$/i, '')}" with the ZIP contents.`); if (!confirmed) return; // Show extracting state const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-btn`); if (btn) { btn.textContent = '⏳ Extracting...'; btn.disabled = true; } await window.app.modules.scanner.extractZip(folder.zipPath); // Auto-refresh preserving tree state await window.app.modules.scanner.scanDirectory(window.app.rootHandle, true); } catch (err) { console.error('Error extracting ZIP:', err); alert('Error extracting ZIP: ' + err.message); // Reset button const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-btn`); if (btn) { btn.textContent = '📤 Extract'; btn.disabled = false; } } } /** * Count ZIP descendants in a folder */ function countZipDescendants(folder) { let count = 0; if (folder.children) { for (const child of folder.children) { if (child.isZipRoot) { count++; } count += countZipDescendants(child); } } return count; } /** * Get all ZIP folders as flat list */ function getZipDescendants(folder, zips = []) { if (folder.children) { for (const child of folder.children) { if (child.isZipRoot) { zips.push(child); } getZipDescendants(child, zips); } } return zips; } /** * Handle extracting all ZIPs in a folder */ async function handleExtractAllZips(folder) { const zips = getZipDescendants(folder); if (zips.length === 0) return; const confirmed = confirm(`Extract ${zips.length} ZIP file(s)?\n\nThis will create folders for each ZIP with their contents.`); if (!confirmed) return; try { // Show extracting state on button const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-all-btn`); if (btn) { btn.textContent = '⏳ Extracting...'; btn.disabled = true; } // Extract all ZIPs for (const zip of zips) { if (zip.zipPath) { await window.app.modules.scanner.extractZip(zip.zipPath); } } // Auto-refresh preserving tree state await window.app.modules.scanner.scanDirectory(window.app.rootHandle, true); } catch (err) { console.error('Error extracting ZIPs:', err); alert('Error extracting ZIPs: ' + err.message); // Reset button const btn = document.querySelector(`.folder-item[data-path="${folder.path}"] .zip-extract-all-btn`); if (btn) { btn.textContent = `📤 Extract All (${zips.length})`; btn.disabled = false; } } } /** * Toggle folder expansion */ function toggleFolder(folder, recursive = false) { folder.expanded = !folder.expanded; if (recursive && folder.children) { // Recursively expand/collapse all children const newState = folder.expanded; function setAllExpanded(f) { f.expanded = newState; if (f.children) { f.children.forEach(setAllExpanded); } } folder.children.forEach(setAllExpanded); } render(); // Opening a not-yet-complete folder jumps its subtree to the front of // the scan so its contents are complete on open (re-renders as it // fills in). Background scanning continues for everything else. if (folder.expanded && folder.scanState !== 'done' && window.app.modules.scanner && window.app.modules.scanner.ensureScanned) { window.app.modules.scanner.ensureScanned(folder).then(render).catch(() => {}); } } /** * Load files from all selected folders */ async function loadFilesFromSelectedFolders() { // Use store to manage files window.app.modules.store.setSelectedFolders(Array.from(window.app.selectedFolders)); } /** * Find folder by path in tree */ function findFolderByPath(path) { function search(folders) { for (const folder of folders) { if (folder.path === path) { return folder; } if (folder.children) { const found = search(folder.children); if (found) return found; } } return null; } return search(window.app.folderTree); } /** * Update selected folders count */ function updateSelectedCount() { const count = window.app.selectedFolders.size; window.app.dom.selectedFoldersCount.textContent = `${count} folder${count !== 1 ? 's' : ''} selected`; } /** * Build folder tree from scanned data */ function buildTree(rootHandle, foldersMap) { const tree = []; // Convert flat map to tree structure function buildNode(handle, path) { // For virtual folders, look up by path string; for real folders, use handle let files; if (handle.isVirtualDir || handle.isZipRoot) { files = foldersMap.get(handle.virtualPath || handle.zipPath) || []; } else { files = foldersMap.get(handle) || []; } // Add folderPath to each file for folder highlighting (filter out null files) files.filter(file => file !== null).forEach(file => { if (!file.isDirectory) { file.folderPath = path; } }); // Filter out null files for the node const validFiles = files.filter(f => f !== null); const node = { name: handle.name, path: path, handle: handle, files: validFiles, fileCount: validFiles.length, children: [], expanded: false }; // Mark ZIP-related nodes if (handle.isZipRoot) { node.isZipRoot = true; node.zipPath = handle.zipPath; } if (handle.isVirtualDir) { node.isVirtualDir = true; node.zipPath = handle.zipPath; } return node; } // Recursively build tree function addChildren(node) { // Get subdirectories (filter out null files first) // For virtual folders, look up by path string let files; if (node.handle.isVirtualDir || node.handle.isZipRoot) { files = foldersMap.get(node.handle.virtualPath || node.handle.zipPath) || []; } else { files = foldersMap.get(node.handle) || []; } const validFiles = files.filter(f => f !== null); const subdirs = validFiles.filter(f => f.isDirectory); subdirs.forEach(subdir => { const childPath = node.path + '/' + subdir.handle.name; const childNode = buildNode(subdir.handle, childPath); addChildren(childNode); node.children.push(childNode); }); // Update file count to exclude directories and null files node.files = validFiles.filter(f => !f.isDirectory); node.fileCount = node.files.length; } // Build root const root = buildNode(rootHandle, rootHandle.name); addChildren(root); // Expand root by default root.expanded = true; tree.push(root); return tree; } /** * Get all currently visible folders (expanded tree) */ function getVisibleFolders() { const visible = []; function traverse(folders) { for (const folder of folders) { visible.push(folder); if (folder.expanded && folder.children) { traverse(folder.children); } } } traverse(window.app.folderTree); return visible; } /** * Select all visible folders */ function selectAllVisible() { const visible = getVisibleFolders(); window.app.selectedFolders.clear(); visible.forEach(f => window.app.selectedFolders.add(f.path)); render(); loadFilesFromSelectedFolders(); } /** * Expand all folders in tree */ function expandAll() { function setAllExpanded(folder) { folder.expanded = true; if (folder.children) { folder.children.forEach(setAllExpanded); } } window.app.folderTree.forEach(setAllExpanded); render(); } /** * Select all folders in tree */ function selectAll() { function collectAllPaths(folders, paths = []) { folders.forEach(folder => { paths.push(folder.path); if (folder.children) { collectAllPaths(folder.children, paths); } }); return paths; } const allPaths = collectAllPaths(window.app.folderTree); allPaths.forEach(path => window.app.selectedFolders.add(path)); render(); loadFilesFromSelectedFolders(); } /** * Set up keyboard shortcuts for folder tree */ function setupKeyboardShortcuts() { const container = window.app.dom.folderTree; container.addEventListener('keydown', (e) => { // Ctrl+A: Select all visible if ((e.ctrlKey || e.metaKey) && e.key === 'a') { e.preventDefault(); selectAllVisible(); } }); // Make container focusable container.tabIndex = 0; } // ── Classify interactions (exclude menu, cross-tree reveal) ───────────── var classifyWired = false; function wireClassifyInteractions() { if (classifyWired) return; classifyWired = true; var ft = window.app.dom.folderTree; if (!ft) { classifyWired = false; return; } ft.addEventListener('contextmenu', onContextMenu); // Single-click a source file → preview it (the "look at it, then assign" // half of the workflow). Drag still assigns; right-click excludes. ft.addEventListener('click', function (e) { if (!classifyOn()) return; var fe = e.target.closest('.file-item'); if (!fe || !fe.dataset.key) return; var file = findFileByKey(fe.dataset.key); if (file && window.app.modules.preview && window.app.modules.preview.previewFile) { window.app.modules.preview.previewFile(file); } }); } // Aggregate classification state across a folder's loaded subtree files. function aggregateState(files) { if (!files.length) return null; var c = window.app.modules.classify; var ex = 0, done = 0, placed = 0; files.forEach(function (f) { var s = c.fileState(f); if (s === 'excluded') ex++; else if (s === 'done') done++; else if (s !== 'none') placed++; }); if (ex === files.length) return 'excluded'; var active = files.length - ex; if (active > 0 && done === active) return 'done'; if (done > 0 || placed > 0) return 'partial'; return 'none'; } function findFolderByPath(path) { var hit = null; (function walk(nodes) { (nodes || []).forEach(function (n) { if (hit) return; if (n.path === path) { hit = n; return; } walk(n.children); }); })(window.app.folderTree); return hit; } function findFileByKey(key) { var c = window.app.modules.classify, hit = null; (function walk(nodes) { (nodes || []).forEach(function (n) { if (hit) return; (n.files || []).forEach(function (f) { if (!hit && c.srcKeyForFile(f) === key) hit = f; }); walk(n.children); }); })(window.app.folderTree); return hit; } function expandToPath(folderPath) { (function walk(nodes) { (nodes || []).forEach(function (n) { if (n.path === folderPath || folderPath.indexOf(n.path + '/') === 0) { n.expanded = true; walk(n.children); } }); })(window.app.folderTree); } // Reveal a source file (target → source). Expands its folder chain, renders, // scrolls + flashes the row. function revealFile(key) { var file = findFileByKey(key); if (!file) return; expandToPath(file.folderPath); render(); var rows = window.app.dom.folderTree.querySelectorAll('.file-item'); var row = Array.prototype.filter.call(rows, function (r) { return r.dataset.key === key; })[0]; if (row) { row.scrollIntoView({ block: 'center' }); row.classList.add('match-highlight'); setTimeout(function () { row.classList.remove('match-highlight'); }, 1500); } } // ── context menu (exclude / include / clear) ─────────────────────────── var menuEl = null; function hideMenu() { if (menuEl) { menuEl.remove(); menuEl = null; } } function showMenu(x, y, items) { hideMenu(); menuEl = document.createElement('div'); menuEl.className = 'cl-menu'; items.forEach(function (it) { var b = document.createElement('button'); b.className = 'cl-menu__item'; b.textContent = it.label; b.addEventListener('click', function () { hideMenu(); it.fn(); }); menuEl.appendChild(b); }); menuEl.style.left = x + 'px'; menuEl.style.top = y + 'px'; document.body.appendChild(menuEl); setTimeout(function () { document.addEventListener('click', hideMenu, { once: true }); document.addEventListener('scroll', hideMenu, { once: true, capture: true }); }, 0); } function onContextMenu(e) { if (!classifyOn()) return; var c = window.app.modules.classify; var fileEl = e.target.closest('.file-item'); var folderEl = e.target.closest('.folder-item'); if (!fileEl && !folderEl) return; e.preventDefault(); var items = []; if (fileEl) { var key = fileEl.dataset.key; var a = c.getAssignment(key); var excluded = !!(a && a.excluded); items.push({ label: excluded ? 'Include in copy' : 'Exclude from copy', fn: function () { c.setExcluded([key], !excluded); } }); if (a && (a.trackingNodeId || a.transmittalNodeId)) { if (a.trackingNodeId) items.push({ label: 'Clear tracking', fn: function () { c.place([key], null, 'tracking'); } }); if (a.transmittalNodeId) items.push({ label: 'Clear transmittal', fn: function () { c.place([key], null, 'transmittal'); } }); } } else { var folder = findFolderByPath(folderEl.dataset.path); var keys = keysFor(subtreeFiles(folder || { files: [], children: [] })); if (!keys.length) return; var allExcl = keys.every(function (k) { var a = c.getAssignment(k); return a && a.excluded; }); items.push({ label: (allExcl ? 'Include' : 'Exclude') + ' folder (' + keys.length + ' file' + (keys.length === 1 ? '' : 's') + ')', fn: function () { c.setExcluded(keys, !allExcl); }, }); } showMenu(e.clientX, e.clientY, items); } // Export module window.app.modules.tree = { render, buildTree, loadFilesFromSelectedFolders, setupKeyboardShortcuts, expandAll, selectAll, revealFile, setShowFilters }; })();