(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 + '/'; // Multi-project mode is opt-in via ?projects= in the URL. // ?projects= absent → not multi-project; scan whatever the URL // points to (single-project or in-archive // mode). The server's project list, if any, // stays out of view. // ?projects= empty → multi-project; include every project the // server says the user can access. // ?projects=A,B → multi-project; include only the listed // projects (intersected with server access). // The archive never sees projects beyond this scope — the visibility // dropdown only narrows what's already in availableProjects. var urlParams = new URLSearchParams(location.search); var projectsParamPresent = urlParams.has('projects'); if (projectsParamPresent) { window.app.isMultiProject = true; // Fetch the server's ACL-filtered project list so we can drop any // listed names the user doesn't actually have access to (and so // the empty-projects= "include everything" mode has a list to use). var serverNames = null; try { var resp = await fetch(baseUrl, { headers: { 'Accept': 'application/json' } }); if (resp.ok) { var serverProjects = await resp.json(); if (Array.isArray(serverProjects) && serverProjects.length > 0 && serverProjects[0] && typeof serverProjects[0].name === 'string') { serverNames = new Set(serverProjects.map(function(p) { return p.name; })); } } } catch (e) { // Plain Caddy or proxy-stripped — trust the URL list as-is. } if (window.app.projectFilter && window.app.projectFilter.size > 0) { // Listed names: intersect with server access, warn about misses. if (serverNames) { var accessible = new Set(); var missing = []; window.app.projectFilter.forEach(function(p) { if (serverNames.has(p)) accessible.add(p); else missing.push(p); }); window.app.projectFilter = accessible; if (missing.length > 0) showProjectWarning(missing); } window.app.availableProjects = Array.from(window.app.projectFilter).sort(); } else if (serverNames) { // Empty ?projects= — scan everything the user can access. window.app.projectFilter = new Set(serverNames); window.app.availableProjects = Array.from(serverNames).sort(); } // else: ?projects= empty AND no server list — leave projectFilter // empty; source.js will fall through to in-archive mode. } // visibleProjects: default to projectFilter (everything visible), or // honor an explicit ?show= from the URL (intersected with projectFilter // to drop names that aren't in scope). An empty ?show= means "hide // everything" — distinct from "no ?show= at all". var showInUrl = urlParams.has('show'); if (showInUrl) { var inScope = new Set(); (window.app.visibleProjects || new Set()).forEach(function(n) { if (window.app.projectFilter.has(n)) inScope.add(n); }); window.app.visibleProjects = inScope; } else { window.app.visibleProjects = new Set(window.app.projectFilter); } 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). Keyed by // party NAME so duplicate third-party folders across projects merge. 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.name); } }); 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__inner'); 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('appContainer'); 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 unique party names. Same-named // third-party folders across multiple projects collapse to one row. // selectedGroupingFolders is a Set of party NAMES (not paths) so toggling // affects every project occurrence at once. 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); // Dedupe by name (keep first occurrence per name). In multi-project mode, // skip parties whose every occurrence is under a hidden project — if at // least one occurrence is in a visible project, the party stays. const seen = new Set(); const uniqueParties = []; for (const f of partyFolders) { if (seen.has(f.name)) continue; if (window.app.isMultiProject) { const hasVisible = partyFolders.some(p => p.name === f.name && pathIsInVisibleProject(p.path) ); if (!hasVisible) continue; } seen.add(f.name); uniqueParties.push(f); } uniqueParties.sort((a, b) => a.name.localeCompare(b.name)); const partyNames = new Set(uniqueParties.map(f => f.name)); // If "Select All" mode is active, auto-select all visible party names. if (window.app.selectAllGroupingFolders) { window.app.selectedGroupingFolders.clear(); uniqueParties.forEach(f => window.app.selectedGroupingFolders.add(f.name)); } else { // Remove selections for names that are no longer visible. for (const selectedName of window.app.selectedGroupingFolders) { if (!partyNames.has(selectedName)) { window.app.selectedGroupingFolders.delete(selectedName); } } } // Sync checkbox state const checkbox = document.getElementById('selectAllGroupingCheckbox'); if (checkbox) checkbox.checked = window.app.selectAllGroupingFolders; if (uniqueParties.length === 0 && window.app.groupingFilter) { container.innerHTML = '
No parties match your filter
'; updateFolderSelectionState('groupingFoldersList'); return; } container.innerHTML = uniqueParties.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. // Off->on triggers a refresh because source.js skips listings for disabled folder types // entirely (no listing fetched), so newly-enabled types need a rescan to surface their data. function toggleFolderType(type) { const wasEnabled = window.app.enabledFolderTypes.has(type); if (wasEnabled) { window.app.enabledFolderTypes.delete(type); } else { window.app.enabledFolderTypes.add(type); } renderFolderTypeBar(); renderGroupingFolders(); renderTransmittalFolders(); window.app.modules.filtering.applyFilters(); window.app.modules.urlState.push(); if (!wasEnabled && window.app.directories.length > 0) { window.app.modules.directory.refreshDirectories(); } } // In multi-project mode, returns true if the path contains a segment matching // a checked project in the picker. Single-project mode always returns true // (no project segment to match against). function pathIsInVisibleProject(path) { if (!window.app.isMultiProject) return true; if (!window.app.visibleProjects || window.app.visibleProjects.size === 0) return false; return path.split('/').some(seg => window.app.visibleProjects.has(seg)); } // Returns true if an outstanding file's actualPath has a path segment matching // any selected party name and is not under a hidden folder type. Segment-equality // (not prefix) so the same party name selected across projects matches all // occurrences regardless of project ID prefix. function outstandingFileIsVisible(file) { const selectedGrouping = window.app.selectedGroupingFolders; if (selectedGrouping.size === 0) return false; if (isUnderHiddenFolderType(file.actualPath)) return false; if (!pathIsInVisibleProject(file.actualPath)) return false; return file.actualPath.split('/').some(seg => selectedGrouping.has(seg)); } // 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 the transmittal folder's path satisfies all three cascade // layers: project visibility, party selection (any path segment matches a // selected party name), and folder-type enablement (no segment is a // folder-type marker that's currently disabled, regardless of where in the // path it sits). Segment-equality matching means a party "BM" selected // matches every "<...>/BM/<...>" path regardless of the prefix; and the // folder-type check covers BOTH the canonical "/Issued/" layout // AND nested layouts like "//Issued/" — a deeper folder- // type marker still triggers the cascade. function transmittalIsUnderVisibleParty(folder) { if (!pathIsInVisibleProject(folder.path)) return false; if (isUnderHiddenFolderType(folder.path)) return false; return folder.path.split('/').some(seg => window.app.selectedGroupingFolders.has(seg) ); } // 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); }); // Default selection: 'base' (un-modified revisions) plus '+C' (comment // markups against base). Other modifier types (+B, +D, …) are // available in the dropdown but hidden by default — users opt them in // via the Modifiers dropdown when they want to see scratch / draft / // hold-style markups. Falls back to selecting whatever is available // when neither default exists, so the table never goes empty out of // the gate. const defaults = ['base', '+C']; const selected = new Set(); defaults.forEach(d => { if (window.app.availableModifiers.has(d)) selected.add(d); }); if (selected.size === 0) { window.app.availableModifiers.forEach(m => selected.add(m)); } window.app.selectedModifiers = selected; // 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, pathIsInVisibleProject, 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); })();