Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 43 additions & 22 deletions src/main/resources/web/browser.js
Original file line number Diff line number Diff line change
Expand Up @@ -317,25 +317,36 @@ function dualBrowser() {
},

/**
* Cross-panel file-match indicator for row highlighting (v0.3.2). Returns one of:
* 'match-same' — file with the same (name, size, mtime) exists on the
* OTHER panel's current view → subtle green tint
* 'match-mtime-diff' — same (name, size) but different mtime → subtle yellow
* tint (might still be the same content, but copy time
* differed; hash check would settle it definitively)
* null — no useful match, no highlight
* Cross-panel match indicator for row highlighting. Returns one of:
* 'match-same' — file: same (name, size, mtime) exists on the OTHER
* panel's current view → subtle green
* 'match-mtime-diff' — file: same (name, size) but different mtime → subtle
* yellow (might be the same content with drifted mtime)
* — dir: a directory with the same name exists on the
* other side → subtle yellow (we can't compare recursive
* size/mtime cheaply, so name-only is the strongest
* signal we offer here; v0.3.3+)
* null — no useful match
*
* Only same-directory comparison: a file at /share/Movies/big.mkv on the left
* is NOT matched against /incoming/big.mkv on the right unless both panels are
* navigated to the matching directory. Dirs are never highlighted.
* Same-directory comparison only: navigate both panels into the matching folder
* to see tints. Hash-based comparison stays out of scope.
*
* @param side 'local' | 'peer' — which panel the row belongs to
* @param e entry being rendered
*/
matchClass(side, e) {
if (!e || e.type !== 'file') return null;
if (!e) return null;
const other = side === 'local' ? this.peer : this.local;
if (!other) return null;
if (e.type === 'dir') {
// Dir mtime is non-recursive (only triggers on add/remove of immediate
// children) and the listing has no recursive size, so any "deeper" check
// would require a fs-walk we don't want to fire on every browse. Yellow
// tint = "a folder with this name exists on the other side; open both
// to compare contents."
return other.dirMatch(e.name) ? 'match-mtime-diff' : null;
}
if (e.type !== 'file') return null;
const m = other.fileMatch(e.name);
if (!m) return null;
if (m.size !== e.size) return null;
Expand Down Expand Up @@ -610,24 +621,34 @@ function makePanel(side) {
return this.entries.length > 0 && this.selection.length === this.entries.length;
},

// Lazy index over file-type entries by name, used by dualBrowser.matchClass
// to highlight cross-panel matches (v0.3.2). Rebuilt on the first lookup
// after this.entries is replaced — `_indexedEntries` is the identity guard.
// Lazy by-type index over the panel's entries, used by dualBrowser.matchClass
// to highlight cross-panel matches. Rebuilt on the first lookup after
// this.entries is replaced — `_indexedEntries` is the identity guard.
// O(N) build, O(1) lookups thereafter; a typical render with 200 files in
// each panel does one rebuild per panel and 200 lookups per side, so
// sub-millisecond.
_fileIndex: null,
// sub-millisecond. Files and dirs live in separate maps because the match
// rules differ (dirs match by name only, files by name+size+mtime).
_entryIndex: null,
_indexedEntries: null,
fileMatch(name) {
if (this._fileIndex == null || this._indexedEntries !== this.entries) {
const idx = new Map();
_ensureIndex() {
if (this._entryIndex == null || this._indexedEntries !== this.entries) {
const files = new Map();
const dirs = new Map();
for (const e of this.entries) {
if (e && e.type === 'file') idx.set(e.name, e);
if (!e) continue;
if (e.type === 'file') files.set(e.name, e);
else if (e.type === 'dir') dirs.set(e.name, e);
}
this._fileIndex = idx;
this._entryIndex = { files, dirs };
this._indexedEntries = this.entries;
}
return this._fileIndex.get(name) || null;
return this._entryIndex;
},
fileMatch(name) {
return this._ensureIndex().files.get(name) || null;
},
dirMatch(name) {
return this._ensureIndex().dirs.get(name) || null;
},

selectedRelPaths() {
Expand Down
Loading