// tree.js — in-memory tree model + DOM rendering. // // Nodes are stored flat in state.nodes (Map by id). The visible // render is a depth-first walk starting from state.rootIds, skipping // children of unexpanded folders. This decouples model from DOM and // keeps re-renders linear in the visible-row count. (function () { 'use strict'; var state = window.app.state; var loader = window.app.modules.loader; // ── Model helpers ──────────────────────────────────────────────────── function newNode(raw, parentId, depth) { var id = state.nextId++; var node = { id: id, name: raw.name, isDir: raw.isDir, size: raw.size, modTime: raw.modTime, ext: raw.ext, url: raw.url, handle: raw.handle, depth: depth, parentId: parentId, expanded: false, loaded: false, childIds: [] }; state.nodes.set(id, node); return node; } function clearTree() { state.nodes.clear(); state.rootIds = []; state.nextId = 1; } // Sort an array of nodes by current sort key. Folders always come // first within a level (mimics common file managers). function sortNodes(ids) { var key = state.sort.key; var dir = state.sort.dir; ids.sort(function (a, b) { var na = state.nodes.get(a); var nb = state.nodes.get(b); // Folders before files if (na.isDir !== nb.isDir) return na.isDir ? -1 : 1; var av, bv; switch (key) { case 'size': av = na.size; bv = nb.size; break; case 'ext': av = na.ext; bv = nb.ext; break; case 'date': av = na.modTime ? na.modTime.getTime() : 0; bv = nb.modTime ? nb.modTime.getTime() : 0; break; default: av = na.name.toLowerCase(); bv = nb.name.toLowerCase(); } if (av < bv) return -1 * dir; if (av > bv) return 1 * dir; return na.name.toLowerCase().localeCompare(nb.name.toLowerCase()); }); } // Populate state with the root listing. function setRoot(rawEntries) { clearTree(); rawEntries.forEach(function (raw) { var n = newNode(raw, null, 0); state.rootIds.push(n.id); }); sortNodes(state.rootIds); } // Populate a folder's children. Caller passes raw entries in any order. function setChildren(parentId, rawEntries) { var parent = state.nodes.get(parentId); if (!parent) return; // Drop any existing children first (re-load case). parent.childIds.forEach(function (id) { state.nodes.delete(id); }); parent.childIds = []; rawEntries.forEach(function (raw) { var n = newNode(raw, parentId, parent.depth + 1); parent.childIds.push(n.id); }); sortNodes(parent.childIds); parent.loaded = true; } // Walk visible nodes in render order. function visibleIds() { var out = []; function walk(ids) { for (var i = 0; i < ids.length; i++) { out.push(ids[i]); var n = state.nodes.get(ids[i]); if (n.isDir && n.expanded) walk(n.childIds); } } // Re-sort everything at all levels so a sort change reorders // already-loaded children consistently. sortNodes(state.rootIds); state.nodes.forEach(function (n) { if (n.isDir && n.loaded) sortNodes(n.childIds); }); walk(state.rootIds); return out; } // ── Rendering ──────────────────────────────────────────────────────── function fmtSize(bytes) { if (bytes == null) return ''; if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } function fmtDate(d) { if (!d) return ''; var pad = function (n) { return n < 10 ? '0' + n : '' + n; }; return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes()); } function escapeHtml(s) { return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function rowHtml(node) { var indent = node.depth * 1.2; var iconChar = node.isDir ? '📁' : '📄'; var labelClass = node.isDir ? 'is-folder' : 'is-file'; var chevronClass = 'tree-name__chevron' + (node.isDir ? '' : ' tree-name__chevron--leaf'); var nameInner; if (node.isDir) { nameInner = '' + escapeHtml(node.name) + ''; } else { // File: clickable link. In server mode, href is a real URL // that opens the file. In FS mode, click handler reads the // file via the handle and triggers a download (Phase 2). var href = node.url || '#'; nameInner = '' + escapeHtml(node.name) + ''; } return '' + '