track list search results now sorted by relevance and highlighted

This commit is contained in:
2026-02-25 01:09:54 -05:00
parent e192d4625e
commit 8f1ee25053
6 changed files with 336 additions and 18 deletions
@@ -23,6 +23,10 @@ import {
DEFAULT_COLUMN_IDS,
} from './columns';
import type { ColumnDef } from './columns';
import {
rankTracks,
highlightText,
} from './search-ranking';
import {
setDragPayload,
emitDragActive,
@@ -131,6 +135,10 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
// -- Memoisation caches for filtered / sorted tracks --
private cachedFilteredTracks: library.Track[] = [];
private cachedSortedTracks: library.Track[] = [];
private cachedRelevanceScores = new Map<
string,
number
>();
private prevFilterTracks: library.Track[] = [];
private prevFilterTerm = '';
private prevFilterColIds = '';
@@ -223,38 +231,66 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
}
private computeFilteredTracks(): library.Track[] {
const term =
this.searchCtrl.term.toLowerCase();
const term = this.searchCtrl.term;
if (!term) return this.tracks;
if (!term) {
this.cachedRelevanceScores.clear();
const cols = this.activeColumns;
return this.tracks;
}
return this.tracks.filter((t) =>
cols.some((col) =>
col
.accessor(t)
.toLowerCase()
.includes(term),
),
const result = rankTracks(
this.tracks,
term,
this.activeColumns,
);
this.cachedRelevanceScores = result.scores;
return result.tracks;
}
private computeSortedTracks(): library.Track[] {
const tracks = this.cachedFilteredTracks;
const hasSearch =
this.cachedRelevanceScores.size > 0;
const col = this.sortField
? COLUMN_DEFS[this.sortField]
: undefined;
const hasColSort = col?.comparator != null;
if (!this.sortField) return tracks;
// No search, no column sort — default order.
if (!hasSearch && !hasColSort) return tracks;
const col = COLUMN_DEFS[this.sortField];
// No search, column sort only — sort by column.
if (!hasSearch && hasColSort) {
const dir =
this.sortDirection === 'asc' ? 1 : -1;
if (!col?.comparator) return tracks;
return [...tracks].sort(
(a, b) =>
dir * col!.comparator!(a, b),
);
}
// Search active — relevance is primary sort,
// column sort (if any) is the tiebreaker.
const scores = this.cachedRelevanceScores;
const dir =
this.sortDirection === 'asc' ? 1 : -1;
return [...tracks].sort(
(a, b) => dir * col.comparator!(a, b),
);
return [...tracks].sort((a, b) => {
const sa = scores.get(a.FilePath) ?? 0;
const sb = scores.get(b.FilePath) ?? 0;
if (sa !== sb) return sb - sa;
if (hasColSort) {
return dir * col!.comparator!(a, b);
}
return 0;
});
}
// =================================================================
@@ -933,6 +969,11 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
text-align: center;
}
.search-match {
background-color: rgba(255, 212, 59, 0.15);
border-radius: 2px;
}
`];
override connectedCallback() {
@@ -1458,10 +1499,15 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH
: col.align === 'right'
? 'cell-right'
: '';
const term =
this.searchCtrl.term;
const display = term
? highlightText(val, term)
: val;
return html`
<div class="cell ${align}">
${val}
${display}
</div>
`;
})}