feat(classifier): direct+total counts in tree; toast scan errors
- Counts now read "direct+total" — e.g. "(2+10 folders, 15+300 files)". The direct number (immediate children) shows as soon as a folder's own directory is read; the total (whole-subtree) is accumulated progressively and flashes grey until the subtree is fully scanned, then goes solid. The "+total" is omitted once done and there's nothing deeper. - Scan errors (permission denied, network hiccups on a share) now surface as a toast (de-duped per path) instead of only console noise; a failed folder/zip is marked done-empty so it doesn't wedge the walk. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ecb0a270cc
commit
3d02084397
3 changed files with 82 additions and 34 deletions
|
|
@ -175,14 +175,14 @@
|
|||
background-color: var(--bg-hover);
|
||||
}
|
||||
|
||||
/* A folder whose subtree isn't fully scanned yet: greyed name + counts,
|
||||
turning solid once scanState hits 'done'. A faint pulse signals active
|
||||
scanning. */
|
||||
.folder-item.scanning .folder-name,
|
||||
.folder-item.scanning .folder-count {
|
||||
color: var(--text-muted, #8a8a8a);
|
||||
/* Counts read "direct+total". The direct number stays solid (immediate info);
|
||||
the "+total" subtree count is muted and pulses while its subtree is still
|
||||
being scanned, then goes solid once final. */
|
||||
.folder-count .ct-total {
|
||||
color: var(--text-secondary, #6b7280);
|
||||
}
|
||||
.folder-item.scanning .folder-count {
|
||||
.folder-count .ct-total.pending {
|
||||
color: var(--text-muted, #9aa0a6);
|
||||
font-style: italic;
|
||||
animation: scan-pulse 1.2s ease-in-out infinite;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,10 +59,10 @@
|
|||
handle: handle,
|
||||
parent: parent || null,
|
||||
files: [],
|
||||
fileCount: 0,
|
||||
subdirCount: 0,
|
||||
totalFiles: 0,
|
||||
totalDirs: 0,
|
||||
fileCount: 0, // direct files in this folder
|
||||
subdirCount: 0, // direct subfolders
|
||||
runFiles: 0, // files in the whole subtree (grows as scanned; final on 'done')
|
||||
runDirs: 0, // subfolders in the whole subtree
|
||||
children: [],
|
||||
expanded: false,
|
||||
scanState: 'pending',
|
||||
|
|
@ -78,11 +78,10 @@
|
|||
// children are done). This is what turns a folder from grey to solid.
|
||||
function markDone(node) {
|
||||
if (node.scanState === 'done') return;
|
||||
// runFiles/runDirs were accumulated into this node (and its ancestors)
|
||||
// as each descendant was scanned, so by the time the subtree is
|
||||
// complete they already hold the final totals — nothing to compute.
|
||||
node.scanState = 'done';
|
||||
let tf = node.fileCount, td = node.children.length;
|
||||
for (const c of node.children) { tf += c.totalFiles; td += c.totalDirs; }
|
||||
node.totalFiles = tf;
|
||||
node.totalDirs = td;
|
||||
const p = node.parent;
|
||||
if (p && p.scanState !== 'done') {
|
||||
p.pending -= 1;
|
||||
|
|
@ -92,6 +91,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
// One-shot toast for scan errors (permission denied, network hiccups on a
|
||||
// share). De-duped per path so a flaky folder doesn't spam.
|
||||
const scanErrorsSeen = new Set();
|
||||
function reportScanError(path, err) {
|
||||
console.error('Scan error:', path, err);
|
||||
if (scanErrorsSeen.has(path)) return;
|
||||
scanErrorsSeen.add(path);
|
||||
const msg = 'Couldn’t scan ' + path + ': ' + (err && err.message ? err.message : err);
|
||||
if (window.zddc && typeof window.zddc.toast === 'function') {
|
||||
window.zddc.toast(msg, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan directory and build folder tree with files
|
||||
*/
|
||||
|
|
@ -237,7 +249,7 @@
|
|||
const zh = { name: zipName, kind: 'directory', isZipRoot: true, zipPath: zipPath };
|
||||
const zipNode = makeNode(zh, zipPath, node);
|
||||
try { await scanZipIntoNode(zipNode, fo); }
|
||||
catch (e) { console.error('Error scanning ZIP:', zipPath, e); }
|
||||
catch (e) { reportScanError(zipPath, e); zipNode.scanState = 'done'; }
|
||||
childDirs.push(zipNode);
|
||||
if (scanStats) scanStats.folders++;
|
||||
}
|
||||
|
|
@ -248,12 +260,23 @@
|
|||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error scanning folder:', node.path, err);
|
||||
node.scanError = true;
|
||||
reportScanError(node.path, err);
|
||||
}
|
||||
node.files = files;
|
||||
node.fileCount = files.length;
|
||||
node.children = childDirs;
|
||||
node.subdirCount = childDirs.length;
|
||||
// Roll this folder's own files/dirs (plus the full contents of any
|
||||
// inline-zip children) into the running subtree totals of this node
|
||||
// and every ancestor. Regular child dirs add their own share when they
|
||||
// get scanned — that's how the total fills in progressively.
|
||||
let addF = files.length;
|
||||
let addD = childDirs.length;
|
||||
for (const c of childDirs) {
|
||||
if (c.scanState === 'done') { addF += c.runFiles; addD += c.runDirs; }
|
||||
}
|
||||
for (let a = node; a; a = a.parent) { a.runFiles += addF; a.runDirs += addD; }
|
||||
// Zip children are scanned inline ('done'); real dirs are still pending.
|
||||
node.pending = childDirs.filter(function (c) { return c.scanState !== 'done'; }).length;
|
||||
if (node.pending === 0) {
|
||||
|
|
@ -316,10 +339,10 @@
|
|||
function finalizeZipNode(node) {
|
||||
node.fileCount = node.files.length;
|
||||
node.subdirCount = node.children.length;
|
||||
let tf = node.fileCount, td = node.children.length;
|
||||
for (const c of node.children) { finalizeZipNode(c); tf += c.totalFiles; td += c.totalDirs; }
|
||||
node.totalFiles = tf;
|
||||
node.totalDirs = td;
|
||||
let rf = node.files.length, rd = node.children.length;
|
||||
for (const c of node.children) { finalizeZipNode(c); rf += c.runFiles; rd += c.runDirs; }
|
||||
node.runFiles = rf;
|
||||
node.runDirs = rd;
|
||||
node.scanState = 'done';
|
||||
node.pending = 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,20 +26,45 @@
|
|||
}
|
||||
|
||||
/**
|
||||
* Count label for a folder row. While the folder is still being scanned
|
||||
* its counts are unknown; once its own directory has been read we show the
|
||||
* immediate subfolder/file counts (greyed until the whole subtree is done).
|
||||
* 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 folderCountLabel(folder) {
|
||||
function populateCount(el, folder) {
|
||||
el.textContent = '';
|
||||
const st = folder.scanState;
|
||||
if (st === 'pending') return '';
|
||||
if (st === 'scanning') return 'scanning…';
|
||||
const d = folder.subdirCount || 0;
|
||||
const f = folder.fileCount || 0;
|
||||
const parts = [];
|
||||
if (d) parts.push(d + (d === 1 ? ' folder' : ' folders'));
|
||||
parts.push(f + (f === 1 ? ' file' : ' files'));
|
||||
return '(' + parts.join(', ') + ')';
|
||||
if (st === 'pending') return;
|
||||
if (st === 'scanning') { el.textContent = 'scanning…'; return; }
|
||||
|
||||
const done = st === '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);
|
||||
frag.appendChild(document.createTextNode(tDir === 1 ? ' folder, ' : ' folders, '));
|
||||
}
|
||||
appendPair(frag, dFile, tFile, done);
|
||||
frag.appendChild(document.createTextNode(tFile === 1 ? ' file)' : ' files)'));
|
||||
el.appendChild(frag);
|
||||
}
|
||||
|
||||
// Append "<direct>" and, when there's a subtree (or scanning is ongoing),
|
||||
// "+<total>" with the total in a span that greys + pulses until final.
|
||||
function appendPair(frag, direct, total, done) {
|
||||
frag.appendChild(document.createTextNode(String(direct)));
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -103,7 +128,7 @@
|
|||
// class until the subtree is fully scanned.
|
||||
const count = document.createElement('span');
|
||||
count.className = 'folder-count';
|
||||
count.textContent = folderCountLabel(folder);
|
||||
populateCount(count, folder);
|
||||
item.appendChild(count);
|
||||
|
||||
// Extract button for ZIP roots
|
||||
|
|
|
|||
Loading…
Reference in a new issue