(function() { 'use strict'; // window.app is initialized in init.js. Reference shape (read-only docs): // directories[], groupingFolders[], transmittalFolders[], files[], // filteredFiles[], selectedFiles:Set, sourceMode ('local'|'http'), // isScanning, scanProgress, // columnFilters {trackingNumber,title,revisions}, columnFilterASTs {...}, // groupingFilter, transmittalFilter, // enabledFolderTypes:Set('issued','received'), // sortField ('trackingNumber'), sortDirection ('asc'|'desc'), // selectedGroupingFolders:Set, selectedTransmittalFolders:Set, // collapsedDateGroups:Set, collapsedGroupingFolders:Set, // selectAllGroupingFolders:bool, selectAllTransmittals:bool, // availableModifiers:Set, selectedModifiers:Set, showSelectedOnly:bool // Parse search terms from filter string function parseSearchTerms(filter) { if (!filter || !filter.trim()) return []; return filter.trim().toLowerCase().split(/\s+/); } // Check if text matches all search terms (AND logic) function matchesSearchTerms(text, terms) { if (!terms || terms.length === 0) return true; return terms.every(term => text.includes(term)); } // Initialize application function initApp() { // Detect source mode from protocol window.app.sourceMode = (location.protocol === 'file:') ? 'local' : 'http'; if (window.app.sourceMode === 'local') { // Check File System Access API support (local mode only) if (!('showDirectoryPicker' in window)) { showUnsupportedBrowserMessage(); return; } } // Set up event listeners window.app.modules.events.setupEventListeners(); // Set up file link handlers (event delegation) window.app.modules.table.setupFileLinkHandlers(); // Apply source-mode-specific UI adjustments applySourceModeUI(); // Restore filter/sort state from URL query string window.app.modules.urlState.restore(); // Initialize UI updateUI(); // Show initial sort indicator window.app.modules.table.updateSortIndicators(); if (window.app.sourceMode === 'http') { // Auto-connect to the server in HTTP mode autoConnectHttpSource(); } else { // Show empty state if no directories (local mode) if (window.app.directories.length === 0) { showEmptyState(); } } } // Apply UI differences based on source mode function applySourceModeUI() { // "Add Local Directory" button is always visible in both modes — // in HTTP mode the user can augment the online archive with local directories. } // Auto-connect to the HTTP server // Derives the base URL from the current page's location async function autoConnectHttpSource() { var href = window.location.href; // Strip query string and fragment href = href.split('?')[0].split('#')[0]; // Strip the filename to get the directory var lastSlash = href.lastIndexOf('/'); var baseUrl = (lastSlash >= 0) ? href.substring(0, lastSlash + 1) : href + '/'; // Check for projects that are in the URL filter but not accessible on the server if (window.app.projectFilter && window.app.projectFilter.size > 0) { try { var resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' } }); if (resp.ok) { var serverProjects = await resp.json(); var accessibleNames = new Set(serverProjects.map(function(p) { return p.name; })); var missing = Array.from(window.app.projectFilter).filter(function(p) { return !accessibleNames.has(p); }); if (missing.length > 0) { showProjectWarning(missing); } } } catch (e) { // Silently ignore — server may not support the project list API } } await addHttpSource(baseUrl); } // Add an HTTP source root (analogous to addDirectory() for local mode) async function addHttpSource(baseUrl) { // Derive a display name from the URL path var urlPath = baseUrl.replace(/\/$/, ''); var rootName = urlPath.substring(urlPath.lastIndexOf('/') + 1) || urlPath; // Check if already added var exists = window.app.directories.some(function(d) { return d.url === baseUrl; }); if (exists) return; window.app.directories.push({ handle: null, name: rootName, path: rootName, url: baseUrl }); if (window.app.directories.length === 1) { hideEmptyState(); } await scanHttpSource(baseUrl, rootName); updateUI(); } // Scan an HTTP source root async function scanHttpSource(baseUrl, rootName) { window.app.isScanning = true; window.app.scanProgress = 'Connecting to server...'; updateStatusBar(); var source = window.app.modules.source.createSource('http', { baseUrl: baseUrl }); var fileCount = 0; var callbacks = { onGroupingFolder: function(folder) { window.app.groupingFolders.push(folder); }, onTransmittalFolder: function(folder) { window.app.transmittalFolders.push(folder); }, onFile: function(file) { window.app.files.push(file); fileCount++; // Throttled progress update — don't update DOM on every file if (fileCount % 10 === 0) { window.app.scanProgress = 'Scanning\u2026 ' + fileCount + ' files found'; updateStatusBar(); } }, onProgress: function() { /* no-op: parallel scan — spinner is enough */ } }; try { await source.scan(baseUrl, callbacks); // Auto-select top-level party folders (shallowest depth) var groupingDepths = window.app.groupingFolders.map(function(f) { return f.path.split('/').length; }); var minGroupingDepth = groupingDepths.length > 0 ? Math.min.apply(null, groupingDepths) : 1; window.app.groupingFolders.forEach(function(folder) { if (folder.path.split('/').length === minGroupingDepth) { window.app.selectedGroupingFolders.add(folder.path); } }); window.app.transmittalFolders.forEach(function(folder) { if (!isUnderHiddenFolderType(folder.path)) { window.app.selectedTransmittalFolders.add(folder.path); } }); ensureOutstandingTransmittal(); // Auto-select Outstanding if selectAllTransmittals is active if (window.app.selectAllTransmittals) { window.app.selectedTransmittalFolders.add('__outstanding__'); } collectModifiers(); updateUI(); window.app.modules.filtering.applyFilters(); if (window.app.modules.presets) { window.app.modules.presets.init(); } } catch (err) { console.error('Error scanning HTTP source:', err); showHttpErrorState(err.message); } finally { window.app.isScanning = false; window.app.scanProgress = ''; updateStatusBar(); } } // Ensure the Outstanding virtual transmittal exists if there are any outstanding files. // Called after each scan completes. Idempotent — safe to call multiple times. function ensureOutstandingTransmittal() { const hasOutstanding = window.app.files.some(f => f.folderPath === '__outstanding__'); const alreadyExists = window.app.transmittalFolders.some(f => f.path === '__outstanding__'); if (hasOutstanding && !alreadyExists) { window.app.transmittalFolders.push({ name: 'Outstanding', path: '__outstanding__', displayPath: 'Outstanding', handle: null, url: null, isVirtual: true }); } } // Show error state when HTTP server is unreachable function showHttpErrorState(message) { var el = document.getElementById('noDirectoryMessage'); if (!el) return; var content = el.querySelector('.empty-state-content'); if (content) { content.innerHTML = '

Could not connect to server

' + '

The archive browser could not retrieve the directory listing from the server.

' + '

Error: ' + escapeHtml(message || 'Unknown error') + '

' + '

Ensure the server is running, CORS is not blocking the request, and Caddy\'s file browsing is enabled.

'; } el.classList.remove('hidden'); } // Show a warning banner listing projects in the URL filter that the user cannot access function showProjectWarning(missingProjects) { var el = document.getElementById('projectWarningBanner'); if (!el || missingProjects.length === 0) return; var list = missingProjects.map(function(p) { return escapeHtml(p); }).join(', '); el.querySelector('.project-warning-text').innerHTML = 'This link includes projects you don\'t have access to: ' + list + ''; el.classList.remove('hidden'); } function dismissProjectWarning() { var el = document.getElementById('projectWarningBanner'); if (el) el.classList.add('hidden'); } // Show unsupported browser message function showUnsupportedBrowserMessage() { const app = document.getElementById('app'); app.innerHTML = `

Browser Not Supported

This application requires a Chromium-based browser (Chrome, Edge, Brave) with File System Access API support.

Please use one of these browsers to access the Archive Browser.

`; } // Show empty state function showEmptyState() { document.getElementById('noDirectoryMessage').classList.remove('hidden'); document.querySelector('.main-container').style.display = 'none'; // Keep header visible document.querySelector('.app-header').style.display = ''; var refreshBtn = document.getElementById('refreshHeaderBtn'); if (refreshBtn) { refreshBtn.classList.add('hidden'); } } // Hide empty state function hideEmptyState() { document.getElementById('noDirectoryMessage').classList.add('hidden'); document.querySelector('.main-container').style.display = ''; var refreshBtn = document.getElementById('refreshHeaderBtn'); if (refreshBtn) { refreshBtn.classList.remove('hidden'); } } // Update UI based on current state function updateUI() { renderFolderTypeBar(); renderFolderLists(); window.app.modules.table.updateFileTable(); updateStatusBar(); } // Render folder lists (rebuilds DOM) function renderFolderLists() { renderGroupingFolders(); renderTransmittalFolders(); } // Check if a folder path is under a hidden folder type // Returns true if any path segment is a known folder type that is NOT currently enabled function isUnderHiddenFolderType(path) { const parts = path.toLowerCase().split('/'); return parts.some(part => window.app.FOLDER_TYPE_NAMES.includes(part) && !window.app.enabledFolderTypes.has(part) ); } // Get filtered grouping folders (single source of truth for filtering logic) function getFilteredGroupingFolders() { const filter = window.app.groupingFilter; return window.app.groupingFolders.filter(folder => { if (isUnderHiddenFolderType(folder.path)) { return false; } if (!filter) return true; const terms = parseSearchTerms(filter); return matchesSearchTerms(folder.name.toLowerCase(), terms); }); } // Render grouping folders as a flat list of party names (depth 1 only) function renderGroupingFolders() { const container = document.getElementById('groupingFoldersList'); // Get filtered grouping folders (uses shared filtering logic) const filteredFolders = getFilteredGroupingFolders(); // Only show top-level party folders (the shallowest depth among all grouping folders) const allDepths = window.app.groupingFolders.map(f => f.path.split('/').length); const minDepth = allDepths.length > 0 ? Math.min(...allDepths) : 1; const partyFolders = filteredFolders.filter(f => f.path.split('/').length === minDepth); // Sort alphabetically partyFolders.sort((a, b) => a.path.localeCompare(b.path)); // Build set of paths for quick lookup const partyPaths = new Set(partyFolders.map(f => f.path)); // If "Select All" mode is active, auto-select all visible party folders if (window.app.selectAllGroupingFolders) { window.app.selectedGroupingFolders.clear(); partyFolders.forEach(f => window.app.selectedGroupingFolders.add(f.path)); } else { // Remove selections for folders that are no longer visible for (const selectedPath of window.app.selectedGroupingFolders) { if (!partyPaths.has(selectedPath)) { window.app.selectedGroupingFolders.delete(selectedPath); } } } // Sync checkbox state const checkbox = document.getElementById('selectAllGroupingCheckbox'); if (checkbox) checkbox.checked = window.app.selectAllGroupingFolders; if (partyFolders.length === 0 && window.app.groupingFilter) { container.innerHTML = '
No parties match your filter
'; updateFolderSelectionState('groupingFoldersList'); return; } container.innerHTML = partyFolders.map(folder => `
${escapeHtml(folder.name)}
`).join(''); updateFolderSelectionState('groupingFoldersList'); } // Render the global folder type toggle bar function renderFolderTypeBar() { const bar = document.getElementById('folderTypeBar'); if (!bar) return; const FOLDER_TYPE_LABELS = { mdl: 'MDL', incoming: 'Incoming', issued: 'Issued', received: 'Received' }; bar.innerHTML = window.app.FOLDER_TYPE_NAMES.map(type => { const active = window.app.enabledFolderTypes.has(type); const label = FOLDER_TYPE_LABELS[type] || (type.charAt(0).toUpperCase() + type.slice(1)); return ``; }).join(''); } // Toggle a folder type on/off globally function toggleFolderType(type) { if (window.app.enabledFolderTypes.has(type)) { window.app.enabledFolderTypes.delete(type); } else { window.app.enabledFolderTypes.add(type); } renderFolderTypeBar(); renderGroupingFolders(); renderTransmittalFolders(); window.app.modules.filtering.applyFilters(); window.app.modules.urlState.push(); } // Returns true if an outstanding file's actualPath is under a selected grouping folder // that is itself visible (not hidden by folder type toggles). function outstandingFileIsVisible(file) { const selectedGrouping = window.app.selectedGroupingFolders; if (selectedGrouping.size === 0) return false; // The actualPath must not be under a hidden folder type if (isUnderHiddenFolderType(file.actualPath)) return false; // The actualPath must be at or under one of the selected grouping folder paths return Array.from(selectedGrouping).some(function(gPath) { return file.actualPath === gPath || file.actualPath.startsWith(gPath + '/'); }); } // Returns true if any outstanding (non-transmittal) files exist under the currently // selected and visible grouping folders. function hasVisibleOutstandingFiles() { return window.app.files.some(function(f) { if (f.folderPath !== '__outstanding__') return false; return outstandingFileIsVisible(f); }); } // Returns true if a transmittal folder is under a selected party and an enabled folder type. // Handles both HTTP paths (party at depth 0) and local paths (party at depth 1+ due to root dir prefix). function transmittalIsUnderVisibleParty(folder) { const parts = folder.path.split('/'); // Find which segment is the party (the one that matches a selected grouping folder path prefix). // The party path is the selected grouping folder path, so check prefix matches. for (const partyPath of window.app.selectedGroupingFolders) { const partyParts = partyPath.split('/'); const partyDepth = partyParts.length; // e.g. 1 for HTTP ("ACME"), 2 for local ("RootDir/ACME") // Check that folder path starts with partyPath if (!folder.path.startsWith(partyPath + '/') && folder.path !== partyPath) continue; // The segment immediately after partyPath is either a folder type or the transmittal itself const remainder = folder.path.substring(partyPath.length + 1); // e.g. "Issued/2025-01-01_..." or "2025-01-01_..." const remainderParts = remainder.split('/'); if (remainderParts.length >= 2) { // There's a folder type segment before the transmittal const folderType = remainderParts[0].toLowerCase(); if (window.app.FOLDER_TYPE_NAMES.includes(folderType)) { // Must be an enabled type return window.app.enabledFolderTypes.has(folderType); } // Unknown folder type — treat as visible return true; } // Transmittal is directly under the party (no folder type level) — always show return true; } // Party not selected return false; } // Render transmittal folders (rebuilds DOM) function renderTransmittalFolders() { const container = document.getElementById('transmittalFoldersList'); const filter = window.app.transmittalFilter; // Filter transmittal folders based on grouping selection and name filter const filteredFolders = window.app.transmittalFolders.filter(folder => { // Outstanding virtual transmittal: include if there are visible outstanding files if (folder.path === '__outstanding__') { if (!hasVisibleOutstandingFiles()) return false; // Apply name filter to "Outstanding" label too if (filter && filter.trim()) { const terms = parseSearchTerms(filter.trim()); if (!matchesSearchTerms('outstanding', terms)) return false; } return true; } // Check name filter let matchesFilter = true; if (filter && filter.trim()) { const terms = parseSearchTerms(filter.trim()); const folderText = folder.name.toLowerCase(); matchesFilter = matchesSearchTerms(folderText, terms); } // If no grouping folders exist at all, show all transmittal folders (flat structure) if (window.app.groupingFolders.length === 0) { return matchesFilter; } // If grouping folders exist but none are selected, show nothing if (window.app.selectedGroupingFolders.size === 0) { return false; } // Check party + folder type visibility return matchesFilter && transmittalIsUnderVisibleParty(folder); }); // Sort regular transmittal folders by date (newest first); Outstanding handled separately const regularFolders = filteredFolders.filter(f => f.path !== '__outstanding__'); regularFolders.sort((a, b) => b.name.localeCompare(a.name)); const showOutstanding = filteredFolders.some(f => f.path === '__outstanding__'); // Build set of visible folder paths (for Select All and deselection logic) const filteredPaths = new Set(filteredFolders.map(f => f.path)); // If "Select All" mode is active, auto-select all visible transmittal folders if (window.app.selectAllTransmittals) { window.app.selectedTransmittalFolders.clear(); filteredFolders.forEach(f => window.app.selectedTransmittalFolders.add(f.path)); } else { // Remove selections for folders that are now filtered out for (const selectedPath of window.app.selectedTransmittalFolders) { if (!filteredPaths.has(selectedPath)) { window.app.selectedTransmittalFolders.delete(selectedPath); } } } // Sync checkbox state const checkbox = document.getElementById('selectAllTransmittalsCheckbox'); if (checkbox) checkbox.checked = window.app.selectAllTransmittals; // Group regular folders by date const foldersByDate = new Map(); regularFolders.forEach(folder => { const match = folder.name.match(/^(\d{4}-\d{2}-\d{2})/); const date = match ? match[1] : 'Unknown'; if (!foldersByDate.has(date)) { foldersByDate.set(date, []); } foldersByDate.get(date).push(folder); }); // Build HTML let html = ''; // Outstanding virtual transmittal — pinned at top if (showOutstanding) { const isSelected = window.app.selectedTransmittalFolders.has('__outstanding__'); html += `
⋯ Outstanding
`; } // Regular date-grouped folders for (const [date, folders] of foldersByDate) { const isCollapsed = window.app.collapsedDateGroups.has(date); const folderCount = folders.length; html += `
${isCollapsed ? '▶' : '▼'} ${escapeHtml(date)} (${folderCount})
`; if (!isCollapsed) { for (const folder of folders) { const match = folder.name.match(/^\d{4}-\d{2}-\d{2}_([^_\s]+)\s*\(([^)]+)\)\s*-\s*(.+)$/); let firstLine = folder.name; let secondLine = ''; if (match) { const [, tracking, status, title] = match; firstLine = `${tracking} • ${status}`; secondLine = title; } html += `
${escapeHtml(firstLine)}
${secondLine ? `
${escapeHtml(secondLine)}
` : ''}
`; } } } if (filteredFolders.length === 0 && window.app.transmittalFilter) { container.innerHTML = '
No folders match your filter
'; updateFolderSelectionState('transmittalFoldersList'); window.app.modules.events.updateToggleAllIcon(); return; } container.innerHTML = html; // Ensure selection state is visually reflected after DOM rebuild updateFolderSelectionState('transmittalFoldersList'); // Update the toggle all icon to reflect current state window.app.modules.events.updateToggleAllIcon(); } // Update status bar function updateStatusBar() { const fileCountEl = document.getElementById('fileCount'); const selectedCountEl = document.getElementById('selectedCount'); // Before any directory is loaded, show a hint instead of "0 files" if (window.app.directories.length === 0 && !window.app.isScanning) { fileCountEl.textContent = 'Select a directory to begin'; selectedCountEl.textContent = ''; document.getElementById('scanStatus').textContent = ''; var spinner2 = document.getElementById('scanSpinner'); if (spinner2) spinner2.classList.add('hidden'); document.getElementById('downloadSelectedBtn').disabled = true; document.getElementById('exportCsvBtn').disabled = true; return; } // Count unique tracking numbers const trackingNumbers = new Set(window.app.filteredFiles.map(f => f.trackingNumber)); const trackingCount = trackingNumbers.size; const fileCount = window.app.filteredFiles.length; // Count files with path errors const pathErrorCount = window.app.filteredFiles.filter(f => f.hasPathError).length; // Format: "X tracking numbers, Y files" + optional path error warning let countText = `${trackingCount} tracking number${trackingCount !== 1 ? 's' : ''}, ${fileCount} file${fileCount !== 1 ? 's' : ''}`; if (pathErrorCount > 0) { countText += ` (⚠️ ${pathErrorCount} inaccessible)`; } fileCountEl.textContent = countText; selectedCountEl.textContent = `${window.app.selectedFiles.size} selected`; document.getElementById('scanStatus').textContent = window.app.scanProgress; var spinner = document.getElementById('scanSpinner'); if (spinner) { spinner.classList.toggle('hidden', !window.app.isScanning); } // Disable action buttons when nothing is selected const noneSelected = window.app.selectedFiles.size === 0; document.getElementById('downloadSelectedBtn').disabled = noneSelected; document.getElementById('exportCsvBtn').disabled = noneSelected; } // Escape HTML for safe insertion function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Update folder selection visual state without rebuilding DOM * This is more efficient than re-rendering when only selection changes * @param {string} containerId - 'groupingFoldersList' or 'transmittalFoldersList' */ function updateFolderSelectionState(containerId) { const container = document.getElementById(containerId); if (!container) { console.warn(`Container not found: ${containerId}`); return; } const selectedSet = containerId === 'groupingFoldersList' ? window.app.selectedGroupingFolders : window.app.selectedTransmittalFolders; // Update selected class on existing elements container.querySelectorAll('.folder-item').forEach(item => { const path = item.getAttribute('data-path'); if (path) { item.classList.toggle('selected', selectedSet.has(path)); } }); } // Extract modifier type from revision string (e.g., "2+B1" -> "+B", "2" -> "base") function getModifierType(revision) { if (!revision) return 'base'; const match = revision.match(/\+([A-Za-z])/); return match ? '+' + match[1].toUpperCase() : 'base'; } // Collect all unique modifiers from files function collectModifiers() { window.app.availableModifiers.clear(); window.app.files.forEach(file => { const modType = getModifierType(file.revision); window.app.availableModifiers.add(modType); }); // Select all by default window.app.selectedModifiers = new Set(window.app.availableModifiers); // Update the dropdown UI renderModifierDropdown(); } // Render the modifier dropdown options function renderModifierDropdown() { const list = document.getElementById('modifierFilterList'); if (!list) return; // Sort modifiers: "base" first, then alphabetically const sorted = Array.from(window.app.availableModifiers).sort((a, b) => { if (a === 'base') return -1; if (b === 'base') return 1; return a.localeCompare(b); }); let html = ''; sorted.forEach(mod => { const checked = window.app.selectedModifiers.has(mod) ? 'checked' : ''; const label = mod === 'base' ? 'Base (no modifier)' : mod; const labelClass = mod === 'base' ? 'modifier-base' : 'modifier-type'; html += `
`; }); list.innerHTML = html; updateModifierSelectAll(); updateModifierButtonLabel(); } // Toggle a specific modifier filter function toggleModifierFilter(mod) { if (window.app.selectedModifiers.has(mod)) { window.app.selectedModifiers.delete(mod); } else { window.app.selectedModifiers.add(mod); } updateModifierSelectAll(); updateModifierButtonLabel(); window.app.modules.filtering.applyFilters(); } // Toggle all modifiers function toggleAllModifiers(selectAll) { if (selectAll) { window.app.selectedModifiers = new Set(window.app.availableModifiers); } else { window.app.selectedModifiers.clear(); } renderModifierDropdown(); window.app.modules.filtering.applyFilters(); } // Update the "Select All" checkbox state function updateModifierSelectAll() { const selectAllCheckbox = document.getElementById('modifierSelectAll'); if (selectAllCheckbox) { selectAllCheckbox.checked = window.app.selectedModifiers.size === window.app.availableModifiers.size; selectAllCheckbox.indeterminate = window.app.selectedModifiers.size > 0 && window.app.selectedModifiers.size < window.app.availableModifiers.size; } } // Update button label to show filter status function updateModifierButtonLabel() { const btn = document.getElementById('modifierFilterBtn'); if (!btn) return; const total = window.app.availableModifiers.size; const selected = window.app.selectedModifiers.size; if (selected === total) { btn.textContent = 'Modifiers ▼'; } else if (selected === 0) { btn.textContent = 'Modifiers (none) ▼'; } else { btn.textContent = `Modifiers (${selected}/${total}) ▼`; } } // Toggle modifier dropdown visibility function toggleModifierDropdown() { const dropdown = document.getElementById('modifierFilterDropdown'); dropdown.classList.toggle('hidden'); } // Update the Folders icon button state based on active visibility toggles function updateFolderVisibilityBtnLabel() { // replaced by renderFolderTypeBar() } // Check if a file passes the modifier filter function filePassesModifierFilter(file) { const modType = getModifierType(file.revision); return window.app.selectedModifiers.has(modType); } // Toggle filter to show only selected files function toggleFilterSelected() { window.app.showSelectedOnly = !window.app.showSelectedOnly; // Update button visual state and label const btn = document.getElementById('filterSelectedBtn'); if (window.app.showSelectedOnly) { btn.classList.add('btn-active'); btn.textContent = 'Show All'; } else { btn.classList.remove('btn-active'); btn.textContent = 'Filter Selected'; } window.app.modules.filtering.applyFilters(); } // Register with module system window.app.modules.app = { updateUI, updateStatusBar, escapeHtml, updateFolderSelectionState, getModifierType, collectModifiers, renderModifierDropdown, toggleModifierFilter, toggleAllModifiers, updateModifierSelectAll, updateModifierButtonLabel, toggleModifierDropdown, updateFolderVisibilityBtnLabel, filePassesModifierFilter, toggleFilterSelected, isUnderHiddenFolderType, ensureOutstandingTransmittal, showHttpErrorState, showUnsupportedBrowserMessage, showProjectWarning, dismissProjectWarning, showEmptyState, hideEmptyState, addHttpSource, scanHttpSource, renderGroupingFolders, renderTransmittalFolders, renderFolderTypeBar, toggleFolderType, outstandingFileIsVisible, hasVisibleOutstandingFiles, transmittalIsUnderVisibleParty, renderFolderLists, getFilteredGroupingFolders, showProjectWarning, dismissProjectWarning, }; // Expose key functions on window for inline HTML handlers window.initApp = initApp; window.toggleFileSelection = function(id) { window.app.modules.table.toggleFileSelection(id); }; window.sortTable = function(f) { window.app.modules.table.sortTable(f); }; window.confirmTransmittal = function() { window.app.modules.dragDrop.confirmTransmittal(); }; window.toggleModifierFilter = toggleModifierFilter; window.toggleFilterSelected = toggleFilterSelected; window.toggleFolderType = toggleFolderType; window.toggleGroupingFolder = function(p, r) { window.app.modules.events.toggleGroupingFolder(p, r); }; window.toggleDateGroup = function(d) { window.app.modules.events.toggleDateGroup(d); }; window.toggleAllDateGroups = function() { window.app.modules.events.toggleAllDateGroups(); }; window.selectAllVisibleFolders = function(t) { window.app.modules.events.selectAllVisibleFolders(t); }; window.removeDirectory = function(n) { window.app.modules.directory.removeDirectory(n); }; window.dismissProjectWarning = dismissProjectWarning; window.verifyFileIntegrity = function(id) { window.app.modules.hash.verifyFileIntegrity(id); }; window.showProjectWarning = showProjectWarning; window.dismissProjectWarning = dismissProjectWarning; // Initialize on DOM ready document.addEventListener('DOMContentLoaded', initApp); })();