diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts index 0de1dba..2676a50 100644 --- a/frontend/src/components/artists-view/artists-view.ts +++ b/frontend/src/components/artists-view/artists-view.ts @@ -172,8 +172,41 @@ export class ArtistsView extends LitElement { }); } - /** Filtered artists based on search term. */ - private get filteredArtists(): library.Artist[] { + // -- Memoisation caches for filtered artists -- + private cachedFilteredArtists: library.Artist[] = + []; + private cachedGridEntries: ArtistEntry[] = []; + private prevFilterArtists: library.Artist[] = []; + private prevFilterTerm = ''; + + /** + * Recompute the filtered-artists and grid-entries + * caches when their inputs have changed. Called + * from willUpdate() so the caches are ready + * before render(). + */ + private recomputeArtistCaches() { + const term = this.searchCtrl.term; + + if ( + this.artists !== this.prevFilterArtists || + term !== this.prevFilterTerm + ) { + this.prevFilterArtists = this.artists; + this.prevFilterTerm = term; + this.cachedFilteredArtists = + this.computeFilteredArtists(); + this.cachedGridEntries = + this.cachedFilteredArtists.map( + (artist, index) => ({ + artist, + index, + }), + ); + } + } + + private computeFilteredArtists(): library.Artist[] { const term = this.searchCtrl.term.toLowerCase(); @@ -186,16 +219,6 @@ export class ArtistsView extends LitElement { ); } - /** Build grid entries from filtered artists. */ - private get gridEntries(): ArtistEntry[] { - return this.filteredArtists.map( - (artist, index) => ({ - artist, - index, - }), - ); - } - static override styles = css` :host { display: flex; @@ -224,7 +247,7 @@ export class ArtistsView extends LitElement { cursor: pointer; transition: background-color 0.15s ease, - transform 0.1s ease; + transform 0.15s ease; overflow: hidden; } @@ -393,6 +416,13 @@ export class ArtistsView extends LitElement { * Lifecycle * ================================================================ */ + override willUpdate( + changed: Map, + ) { + super.willUpdate(changed); + this.recomputeArtistCaches(); + } + override connectedCallback() { super.connectedCallback(); this.loadCardSize(); @@ -539,7 +569,7 @@ export class ArtistsView extends LitElement { const safeIndex = Math.min( saved, - this.filteredArtists.length - 1, + this.cachedFilteredArtists.length - 1, ); if (safeIndex <= 0) { @@ -724,7 +754,7 @@ export class ArtistsView extends LitElement { from: number, to: number, ): Set { - const filtered = this.filteredArtists; + const filtered = this.cachedFilteredArtists; const start = Math.min(from, to); const end = Math.max(from, to); const ids = new Set(); @@ -1282,7 +1312,7 @@ export class ArtistsView extends LitElement { `; } - const entries = this.gridEntries; + const entries = this.cachedGridEntries; if (entries.length === 0) { return html` diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index 6ad992d..68e80fe 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -261,11 +261,41 @@ export class CoverGrid extends LitElement { string[] >(); + // -- Memoisation caches for filtered albums -- + private cachedFilteredAlbums: library.Album[] = []; + private prevFilterAlbums: library.Album[] = []; + private prevFilterTerm = ''; + private prevSortField: AlbumSortField = 'name'; + private prevSortDir: SortDirection = 'asc'; + // ================================================================= - // Filtered albums (search) + // Filtered albums (memoised) // ================================================================= - private get filteredAlbums(): library.Album[] { + /** + * Recompute the filtered-albums cache when its + * inputs have changed. Called from willUpdate() + * so the cache is ready before render(). + */ + private recomputeAlbumCache() { + const term = this.searchCtrl.term; + + if ( + this.albums !== this.prevFilterAlbums || + term !== this.prevFilterTerm || + this.sortField !== this.prevSortField || + this.sortDirection !== this.prevSortDir + ) { + this.prevFilterAlbums = this.albums; + this.prevFilterTerm = term; + this.prevSortField = this.sortField; + this.prevSortDir = this.sortDirection; + this.cachedFilteredAlbums = + this.computeFilteredAlbums(); + } + } + + private computeFilteredAlbums(): library.Album[] { const term = this.searchCtrl.term.toLowerCase(); @@ -448,7 +478,9 @@ export class CoverGrid extends LitElement { cursor: pointer; border-radius: 8px; padding: 5px; - transition: background-color 0.2s ease; + transition: + background-color 0.2s ease, + transform 0.15s ease; box-sizing: border-box; width: var(--card-width, 176px); } @@ -987,6 +1019,7 @@ export class CoverGrid extends LitElement { changed: Map, ) { super.willUpdate(changed); + this.recomputeAlbumCache(); // When the parent provides a new external album // list, update local albums and reset selection. @@ -1040,7 +1073,7 @@ export class CoverGrid extends LitElement { // visual position. if (this.expandedAlbumId !== null) { const filtered = - this.filteredAlbums; + this.cachedFilteredAlbums; const idx = filtered.findIndex( (a) => a.ID === @@ -1328,7 +1361,7 @@ export class CoverGrid extends LitElement { this.expandedAlbumId !== null ) { const idx = - this.filteredAlbums.findIndex( + this.cachedFilteredAlbums.findIndex( (a) => a.ID === this @@ -1554,7 +1587,7 @@ export class CoverGrid extends LitElement { const safeIndex = Math.min( saved, - this.filteredAlbums.length - 1, + this.cachedFilteredAlbums.length - 1, ); if (safeIndex <= 0) return; @@ -1773,7 +1806,7 @@ export class CoverGrid extends LitElement { ) { const pad = CoverGrid.GRID_PADDING; const cols = this.currentColumnCount; - const filtered = this.filteredAlbums; + const filtered = this.cachedFilteredAlbums; // Prefer the expanded album as focus. if (this.expandedAlbumId !== null) { @@ -1883,7 +1916,7 @@ export class CoverGrid extends LitElement { private getCaratOffset(): number { if (this.expandedAlbumId === null) return 0; - const idx = this.filteredAlbums.findIndex( + const idx = this.cachedFilteredAlbums.findIndex( (a) => a.ID === this.expandedAlbumId, ); @@ -1914,7 +1947,7 @@ export class CoverGrid extends LitElement { * "before" virtualizer; the rest go into "after". */ private computeSplitIndex() { - const filtered = this.filteredAlbums; + const filtered = this.cachedFilteredAlbums; if (this.expandedAlbumId === null) { this.splitIndex = filtered.length; @@ -1952,7 +1985,7 @@ export class CoverGrid extends LitElement { * component state (e.g. selectedAlbums) changes. */ private buildGridEntries(): GridEntry[] { - const filtered = this.filteredAlbums; + const filtered = this.cachedFilteredAlbums; const entries: GridEntry[] = []; for (let i = 0; i < filtered.length; i++) { @@ -2239,7 +2272,7 @@ export class CoverGrid extends LitElement { return; } - const filtered = this.filteredAlbums; + const filtered = this.cachedFilteredAlbums; const expandedIndex = filtered.findIndex( (a) => a.ID === this.expandedAlbumId, ); @@ -2346,7 +2379,7 @@ export class CoverGrid extends LitElement { from: number, to: number, ): Set { - const filtered = this.filteredAlbums; + const filtered = this.cachedFilteredAlbums; const start = Math.min(from, to); const end = Math.max(from, to); const ids = new Set(); @@ -2574,7 +2607,7 @@ export class CoverGrid extends LitElement { private syncDropdownToSelection() { if (this.selectedAlbums.size === 1) { const [albumId] = this.selectedAlbums; - const album = this.filteredAlbums.find( + const album = this.cachedFilteredAlbums.find( (a) => a.ID === albumId, ); @@ -2599,7 +2632,7 @@ export class CoverGrid extends LitElement { e: Event, ): { album: library.Album; index: number } | null { const path = e.composedPath(); - const filtered = this.filteredAlbums; + const filtered = this.cachedFilteredAlbums; for (const el of path) { if ( @@ -2775,7 +2808,7 @@ export class CoverGrid extends LitElement { if (raw === undefined) return; const index = parseInt(raw, 10); - const album = this.filteredAlbums[index]; + const album = this.cachedFilteredAlbums[index]; if (album && img.src !== album.CoverArtPath) { img.src = album.CoverArtPath; @@ -3494,7 +3527,7 @@ export class CoverGrid extends LitElement { `; } - if (this.filteredAlbums.length === 0) { + if (this.cachedFilteredAlbums.length === 0) { return html`

No albums match your search.

diff --git a/frontend/src/components/genres-view/genres-view.ts b/frontend/src/components/genres-view/genres-view.ts index 537668f..445445b 100644 --- a/frontend/src/components/genres-view/genres-view.ts +++ b/frontend/src/components/genres-view/genres-view.ts @@ -175,8 +175,40 @@ export class GenresView extends LitElement { }); } - /** Filtered genres based on search term. */ - private get filteredGenres(): Genre[] { + // -- Memoisation caches for filtered genres -- + private cachedFilteredGenres: Genre[] = []; + private cachedGridEntries: GenreEntry[] = []; + private prevFilterGenres: Genre[] = []; + private prevFilterTerm = ''; + + /** + * Recompute the filtered-genres and grid-entries + * caches when their inputs have changed. Called + * from willUpdate() so the caches are ready + * before render(). + */ + private recomputeGenreCaches() { + const term = this.searchCtrl.term; + + if ( + this.genres !== this.prevFilterGenres || + term !== this.prevFilterTerm + ) { + this.prevFilterGenres = this.genres; + this.prevFilterTerm = term; + this.cachedFilteredGenres = + this.computeFilteredGenres(); + this.cachedGridEntries = + this.cachedFilteredGenres.map( + (genre, index) => ({ + genre, + index, + }), + ); + } + } + + private computeFilteredGenres(): Genre[] { const term = this.searchCtrl.term.toLowerCase(); @@ -189,16 +221,6 @@ export class GenresView extends LitElement { ); } - /** Build grid entries from filtered genres. */ - private get gridEntries(): GenreEntry[] { - return this.filteredGenres.map( - (genre, index) => ({ - genre, - index, - }), - ); - } - static override styles = css` :host { display: flex; @@ -227,7 +249,7 @@ export class GenresView extends LitElement { cursor: pointer; transition: background-color 0.15s ease, - transform 0.1s ease; + transform 0.15s ease; overflow: hidden; } @@ -399,6 +421,13 @@ export class GenresView extends LitElement { * Lifecycle * ================================================================ */ + override willUpdate( + changed: Map, + ) { + super.willUpdate(changed); + this.recomputeGenreCaches(); + } + override connectedCallback() { super.connectedCallback(); this.loadCardSize(); @@ -583,7 +612,7 @@ export class GenresView extends LitElement { const safeIndex = Math.min( saved, - this.filteredGenres.length - 1, + this.cachedFilteredGenres.length - 1, ); if (safeIndex <= 0) { @@ -768,7 +797,7 @@ export class GenresView extends LitElement { from: number, to: number, ): Set { - const filtered = this.filteredGenres; + const filtered = this.cachedFilteredGenres; const start = Math.min(from, to); const end = Math.max(from, to); const names = new Set(); @@ -1294,7 +1323,7 @@ export class GenresView extends LitElement { `; } - const entries = this.gridEntries; + const entries = this.cachedGridEntries; if (entries.length === 0) { return html`