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);
|
background-color: var(--bg-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A folder whose subtree isn't fully scanned yet: greyed name + counts,
|
/* Counts read "direct+total". The direct number stays solid (immediate info);
|
||||||
turning solid once scanState hits 'done'. A faint pulse signals active
|
the "+total" subtree count is muted and pulses while its subtree is still
|
||||||
scanning. */
|
being scanned, then goes solid once final. */
|
||||||
.folder-item.scanning .folder-name,
|
.folder-count .ct-total {
|
||||||
.folder-item.scanning .folder-count {
|
color: var(--text-secondary, #6b7280);
|
||||||
color: var(--text-muted, #8a8a8a);
|
|
||||||
}
|
}
|
||||||
.folder-item.scanning .folder-count {
|
.folder-count .ct-total.pending {
|
||||||
|
color: var(--text-muted, #9aa0a6);
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
animation: scan-pulse 1.2s ease-in-out infinite;
|
animation: scan-pulse 1.2s ease-in-out infinite;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,10 @@
|
||||||
handle: handle,
|
handle: handle,
|
||||||
parent: parent || null,
|
parent: parent || null,
|
||||||
files: [],
|
files: [],
|
||||||
fileCount: 0,
|
fileCount: 0, // direct files in this folder
|
||||||
subdirCount: 0,
|
subdirCount: 0, // direct subfolders
|
||||||
totalFiles: 0,
|
runFiles: 0, // files in the whole subtree (grows as scanned; final on 'done')
|
||||||
totalDirs: 0,
|
runDirs: 0, // subfolders in the whole subtree
|
||||||
children: [],
|
children: [],
|
||||||
expanded: false,
|
expanded: false,
|
||||||
scanState: 'pending',
|
scanState: 'pending',
|
||||||
|
|
@ -78,11 +78,10 @@
|
||||||
// children are done). This is what turns a folder from grey to solid.
|
// children are done). This is what turns a folder from grey to solid.
|
||||||
function markDone(node) {
|
function markDone(node) {
|
||||||
if (node.scanState === 'done') return;
|
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';
|
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;
|
const p = node.parent;
|
||||||
if (p && p.scanState !== 'done') {
|
if (p && p.scanState !== 'done') {
|
||||||
p.pending -= 1;
|
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
|
* Scan directory and build folder tree with files
|
||||||
*/
|
*/
|
||||||
|
|
@ -237,7 +249,7 @@
|
||||||
const zh = { name: zipName, kind: 'directory', isZipRoot: true, zipPath: zipPath };
|
const zh = { name: zipName, kind: 'directory', isZipRoot: true, zipPath: zipPath };
|
||||||
const zipNode = makeNode(zh, zipPath, node);
|
const zipNode = makeNode(zh, zipPath, node);
|
||||||
try { await scanZipIntoNode(zipNode, fo); }
|
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);
|
childDirs.push(zipNode);
|
||||||
if (scanStats) scanStats.folders++;
|
if (scanStats) scanStats.folders++;
|
||||||
}
|
}
|
||||||
|
|
@ -248,12 +260,23 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error scanning folder:', node.path, err);
|
node.scanError = true;
|
||||||
|
reportScanError(node.path, err);
|
||||||
}
|
}
|
||||||
node.files = files;
|
node.files = files;
|
||||||
node.fileCount = files.length;
|
node.fileCount = files.length;
|
||||||
node.children = childDirs;
|
node.children = childDirs;
|
||||||
node.subdirCount = childDirs.length;
|
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.
|
// Zip children are scanned inline ('done'); real dirs are still pending.
|
||||||
node.pending = childDirs.filter(function (c) { return c.scanState !== 'done'; }).length;
|
node.pending = childDirs.filter(function (c) { return c.scanState !== 'done'; }).length;
|
||||||
if (node.pending === 0) {
|
if (node.pending === 0) {
|
||||||
|
|
@ -316,10 +339,10 @@
|
||||||
function finalizeZipNode(node) {
|
function finalizeZipNode(node) {
|
||||||
node.fileCount = node.files.length;
|
node.fileCount = node.files.length;
|
||||||
node.subdirCount = node.children.length;
|
node.subdirCount = node.children.length;
|
||||||
let tf = node.fileCount, td = node.children.length;
|
let rf = node.files.length, rd = node.children.length;
|
||||||
for (const c of node.children) { finalizeZipNode(c); tf += c.totalFiles; td += c.totalDirs; }
|
for (const c of node.children) { finalizeZipNode(c); rf += c.runFiles; rd += c.runDirs; }
|
||||||
node.totalFiles = tf;
|
node.runFiles = rf;
|
||||||
node.totalDirs = td;
|
node.runDirs = rd;
|
||||||
node.scanState = 'done';
|
node.scanState = 'done';
|
||||||
node.pending = 0;
|
node.pending = 0;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,20 +26,45 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Count label for a folder row. While the folder is still being scanned
|
* Populate a folder row's count element with "direct+total" counts, e.g.
|
||||||
* its counts are unknown; once its own directory has been read we show the
|
* "(2+10 folders, 15+300 files)" — direct (immediate children) shows as
|
||||||
* immediate subfolder/file counts (greyed until the whole subtree is done).
|
* 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;
|
const st = folder.scanState;
|
||||||
if (st === 'pending') return '';
|
if (st === 'pending') return;
|
||||||
if (st === 'scanning') return 'scanning…';
|
if (st === 'scanning') { el.textContent = 'scanning…'; return; }
|
||||||
const d = folder.subdirCount || 0;
|
|
||||||
const f = folder.fileCount || 0;
|
const done = st === 'done';
|
||||||
const parts = [];
|
const dDir = folder.subdirCount || 0, tDir = folder.runDirs || 0;
|
||||||
if (d) parts.push(d + (d === 1 ? ' folder' : ' folders'));
|
const dFile = folder.fileCount || 0, tFile = folder.runFiles || 0;
|
||||||
parts.push(f + (f === 1 ? ' file' : ' files'));
|
|
||||||
return '(' + parts.join(', ') + ')';
|
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.
|
// class until the subtree is fully scanned.
|
||||||
const count = document.createElement('span');
|
const count = document.createElement('span');
|
||||||
count.className = 'folder-count';
|
count.className = 'folder-count';
|
||||||
count.textContent = folderCountLabel(folder);
|
populateCount(count, folder);
|
||||||
item.appendChild(count);
|
item.appendChild(count);
|
||||||
|
|
||||||
// Extract button for ZIP roots
|
// Extract button for ZIP roots
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue