ZDDC/classifier/js/app.js
ZDDC 22c142e45a chore(headers): standardize across all 7 tools
Bring every tool's header in line with archive's pattern:

  [logo] [title] [version] [Add Local Directory] [⟳] ............... [◐] [?]
  ------------- header-left ---------------       ----- header-right -

Changes per tool:

* browse: rename "Select Directory" → "Add Local Directory"; add the
  red-non-stable wrap to the build label (was missing); add a help
  panel + bundle shared/help.js.

* classifier: rename selectDirectoryBtn → addDirectoryBtn,
  refreshBtn → refreshHeaderBtn for consistency. Update all JS
  callers and welcome-screen copy to the new label.

* mdedit: same id rename. Move the previously-in-pane refresh
  button into the header. Stop renaming the dir button to
  "Directory: <name>" once a folder is loaded — instead use the
  shared btn--subtle variant to de-emphasize while keeping the
  standard label.

* transmittal: convert non-standard <div class="app-header"> with
  spacer/icons containers to <header class="app-header"> with the
  canonical header-left/header-right pair. Move the publish split-
  button into header-left (Transmittal-specific primary action).
  Remove dead .app-header__spacer/__icons/header-icon-btn CSS now
  that nothing references those classes.

* landing, form: add help-btn + help-panel + bundle shared/help.js.
  Each panel is tool-specific (project picker docs for landing,
  schema-driven form docs for form).

Cross-cutting:

* shared/base.css: promote .btn--subtle from browse/css/tree.css
  so any tool with an online mode can de-emphasize Add Local
  Directory consistently.

Verified all 7 tools in headless Chromium: header structure correct,
build label red on non-stable cuts, help panel opens + closes via
button + Esc.
2026-05-04 07:49:17 -05:00

449 lines
14 KiB
JavaScript

/**
* ZDDC Classifier - Main Application
* Spreadsheet-based file renaming with Excel-like formulas
*/
(function() {
'use strict';
// Global application state
window.app = {
// File System
rootHandle: null,
// Data
folderTree: [],
selectedFolders: new Set(), // Multi-select support
lastSelectedFolderPath: null,
hideCompliant: false,
calculateSha256: false,
// DOM elements (populated on init)
dom: {},
// Modules (populated by other files)
modules: {}
};
/**
* Initialize the application
*/
function init() {
// Check browser compatibility
if (!checkBrowserCompatibility()) {
showBrowserWarning();
return;
}
// Cache DOM elements
cacheDOMElements();
// Set up event listeners
setupEventListeners();
// Show welcome screen
showWelcomeScreen();
}
/**
* Check if browser supports File System Access API
*/
function checkBrowserCompatibility() {
return 'showDirectoryPicker' in window;
}
/**
* Show browser compatibility warning
*/
function showBrowserWarning() {
const warning = document.getElementById('browserWarning');
const selectBtn = document.getElementById('addDirectoryBtn');
if (warning) {
warning.classList.remove('hidden');
}
if (selectBtn) {
selectBtn.disabled = true;
selectBtn.textContent = 'Browser Not Supported';
}
}
/**
* Cache DOM element references
*/
function cacheDOMElements() {
app.dom = {
// Screens
welcomeScreen: document.getElementById('welcomeScreen'),
mainApp: document.getElementById('mainApp'),
// Header buttons
addDirectoryBtn: document.getElementById('addDirectoryBtn'),
refreshHeaderBtn: document.getElementById('refreshHeaderBtn'),
saveAllBtn: document.getElementById('saveAllBtn'),
cancelAllBtn: document.getElementById('cancelAllBtn'),
exportHashesBtn: document.getElementById('exportHashesBtn'),
sha256Checkbox: document.getElementById('sha256Checkbox'),
hideCompliantCheckbox: document.getElementById('hideCompliantCheckbox'),
// Folder tree
folderTree: document.getElementById('folderTree'),
folderTreePane: document.getElementById('folderTreePane'),
collapseTreeBtn: document.getElementById('collapseTreeBtn'),
autoScrollCheckbox: document.getElementById('autoScrollCheckbox'),
selectedFoldersCount: document.getElementById('selectedFoldersCount'),
// Spreadsheet
spreadsheet: document.getElementById('spreadsheet'),
spreadsheetBody: document.getElementById('spreadsheetBody'),
sha256Column: document.getElementById('sha256Column'),
// Stats
totalFiles: document.getElementById('totalFiles'),
modifiedFiles: document.getElementById('modifiedFiles'),
errorFiles: document.getElementById('errorFiles'),
// Preview
togglePreviewBtn: document.getElementById('togglePreviewBtn')
};
}
/**
* Set up event listeners
*/
function setupEventListeners() {
// Directory selection
app.dom.addDirectoryBtn.addEventListener('click', handleSelectDirectory);
app.dom.refreshHeaderBtn.addEventListener('click', handleRefresh);
// Drag and drop on welcome screen
setupWelcomeDragDrop();
// Bulk actions
app.dom.saveAllBtn.addEventListener('click', handleSaveAll);
app.dom.cancelAllBtn.addEventListener('click', handleCancelAll);
// Export hashes
app.dom.exportHashesBtn.addEventListener('click', handleExportHashes);
// SHA256 toggle
app.dom.sha256Checkbox.addEventListener('change', handleSha256Toggle);
// Hide compliant toggle
app.dom.hideCompliantCheckbox.addEventListener('change', handleHideCompliantToggle);
// Collapse tree button
app.dom.collapseTreeBtn.addEventListener('click', handleCollapseTree);
// Keyboard shortcuts
document.addEventListener('keydown', handleKeyDown);
// Resize handle
setupResizeHandle();
}
/**
* Handle collapse/expand folder tree pane
*/
function handleCollapseTree() {
const pane = app.dom.folderTreePane;
const btn = app.dom.collapseTreeBtn;
pane.classList.toggle('collapsed');
if (pane.classList.contains('collapsed')) {
// Clear any inline width from resize handle
pane.style.width = '';
btn.textContent = '▶';
btn.title = 'Expand folder tree';
} else {
btn.textContent = '◀';
btn.title = 'Collapse folder tree';
}
}
/**
* Set up folder tree resize handle
*/
function setupResizeHandle() {
const handle = document.getElementById('treeResizeHandle');
const pane = document.getElementById('folderTreePane');
if (!handle || !pane) return;
let isResizing = false;
let startX = 0;
let startWidth = 0;
handle.addEventListener('mousedown', (e) => {
isResizing = true;
startX = e.clientX;
startWidth = pane.offsetWidth;
document.body.style.cursor = 'col-resize';
e.preventDefault();
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const delta = e.clientX - startX;
const newWidth = startWidth + delta;
// Respect min width only
if (newWidth >= 150) {
pane.style.width = newWidth + 'px';
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
}
});
}
/**
* Set up drag-and-drop on the welcome screen
*/
function setupWelcomeDragDrop() {
const screen = app.dom.welcomeScreen;
if (!screen) return;
['dragenter', 'dragover'].forEach(evt => {
screen.addEventListener(evt, (e) => {
e.preventDefault();
screen.classList.add('drag-over');
});
});
['dragleave', 'drop'].forEach(evt => {
screen.addEventListener(evt, (e) => {
e.preventDefault();
screen.classList.remove('drag-over');
});
});
screen.addEventListener('drop', async (e) => {
const item = e.dataTransfer.items && e.dataTransfer.items[0];
if (!item) return;
const handle = await item.getAsFileSystemHandle();
if (!handle || handle.kind !== 'directory') {
alert('Please drop a folder, not a file.');
return;
}
await openDirectory(handle);
});
}
/**
* Handle directory selection via button click
*/
async function handleSelectDirectory() {
try {
const dirHandle = await window.showDirectoryPicker();
await openDirectory(dirHandle);
} catch (err) {
if (err.name !== 'AbortError') {
console.error('Error selecting directory:', err);
alert('Error selecting directory: ' + err.message);
}
}
}
/**
* Open a directory handle and initialize the application
*/
async function openDirectory(dirHandle) {
app.rootHandle = dirHandle;
// Hide welcome screen and show main UI
hideWelcomeScreen();
showMainUI();
// Initialize modules BEFORE scanning (so they're ready for store updates)
app.modules.spreadsheet.init(); // Subscribe to store
app.modules.selection.init();
app.modules.preview.init(); // After selection so it can listen for rowfocused
app.modules.resize.init();
app.modules.filter.init();
app.modules.sort.init();
app.modules.tree.setupKeyboardShortcuts();
// Now scan directory (this will trigger store updates and renders)
await app.modules.scanner.scanDirectory(dirHandle);
// Show refresh button now that a directory is loaded
if (app.dom.refreshHeaderBtn) { app.dom.refreshHeaderBtn.classList.remove('hidden'); }
}
/**
* Handle Refresh button - rescan current directory
*/
async function handleRefresh() {
if (!app.rootHandle) {
alert('No directory selected');
return;
}
try {
// Clear current data
app.folderTree = [];
app.selectedFolders.clear();
app.lastSelectedFolderPath = null;
// Reset store
app.modules.store.reset();
// Rescan directory (modules already initialized, just rescan)
await app.modules.scanner.scanDirectory(app.rootHandle);
} catch (err) {
console.error('Error refreshing directory:', err);
alert('Error refreshing directory: ' + err.message);
}
}
/**
* Handle Save All button
*/
async function handleSaveAll() {
if (!confirm('Save all modified files?')) return;
try {
app.dom.saveAllBtn.disabled = true;
await app.modules.spreadsheet.saveAllFiles();
} catch (err) {
console.error('Error saving files:', err);
alert('Error saving files: ' + err.message);
} finally {
app.dom.saveAllBtn.disabled = false;
}
}
/**
* Handle Cancel All button
*/
function handleCancelAll() {
if (!confirm('Cancel all changes?')) return;
app.modules.spreadsheet.cancelAllChanges();
}
/**
* Handle Export Hashes button
*/
function handleExportHashes() {
app.modules.excel.exportHashes();
}
/**
* Handle SHA256 checkbox toggle
*/
function handleSha256Toggle() {
app.calculateSha256 = app.dom.sha256Checkbox.checked;
// Show/hide SHA256 column
if (app.calculateSha256) {
app.dom.sha256Column.classList.remove('hidden');
} else {
app.dom.sha256Column.classList.add('hidden');
}
// Re-render table
app.modules.spreadsheet.render();
}
/**
* Handle Hide Compliant checkbox toggle
*/
function handleHideCompliantToggle() {
app.hideCompliant = app.dom.hideCompliantCheckbox.checked;
app.modules.store.setHideCompliant(app.hideCompliant);
}
/**
* Handle keyboard shortcuts
*/
function handleKeyDown(e) {
// Ctrl+S - Save All
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
if (!app.dom.saveAllBtn.disabled) {
handleSaveAll();
}
}
// Escape - Cancel editing
if (e.key === 'Escape') {
app.modules.spreadsheet.cancelEditing();
}
}
/**
* Show welcome screen (empty-state overlay)
*/
function showWelcomeScreen() {
if (app.dom.welcomeScreen) {
app.dom.welcomeScreen.classList.remove('hidden');
}
}
/**
* Hide welcome screen (empty-state overlay)
*/
function hideWelcomeScreen() {
if (app.dom.welcomeScreen) {
app.dom.welcomeScreen.classList.add('hidden');
}
}
/**
* Show main UI (no-op: main UI is always rendered)
*/
function showMainUI() {
// Main app is always visible; only the empty-state overlay is toggled
}
/**
* Update stats display
*/
function updateStats() {
const files = app.modules.store.getDisplayFiles();
const totalFiles = files.length;
const modifiedFiles = files.filter(f => f.isDirty).length;
const errorFiles = files.filter(f => f.error).length;
app.dom.totalFiles.textContent = `${totalFiles} file${totalFiles !== 1 ? 's' : ''}`;
app.dom.modifiedFiles.textContent = `${modifiedFiles} modified`;
if (errorFiles > 0) {
app.dom.errorFiles.textContent = `${errorFiles} error${errorFiles !== 1 ? 's' : ''}`;
app.dom.errorFiles.classList.remove('hidden');
} else {
app.dom.errorFiles.classList.add('hidden');
}
// Enable/disable bulk action buttons
app.dom.saveAllBtn.disabled = modifiedFiles === 0;
app.dom.cancelAllBtn.disabled = modifiedFiles === 0;
// Enable/disable export hashes button
app.dom.exportHashesBtn.disabled = totalFiles === 0 || !app.calculateSha256;
}
// Export functions for use by other modules
app.modules.app = {
updateStats
};
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();