added slide animation to grid views

This commit is contained in:
2026-02-23 23:54:42 -05:00
parent 153cd0eae0
commit d55cfb2411
3 changed files with 140 additions and 48 deletions
@@ -172,8 +172,41 @@ export class ArtistsView extends LitElement {
}); });
} }
/** Filtered artists based on search term. */ // -- Memoisation caches for filtered artists --
private get filteredArtists(): library.Artist[] { 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 = const term =
this.searchCtrl.term.toLowerCase(); 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` static override styles = css`
:host { :host {
display: flex; display: flex;
@@ -224,7 +247,7 @@ export class ArtistsView extends LitElement {
cursor: pointer; cursor: pointer;
transition: transition:
background-color 0.15s ease, background-color 0.15s ease,
transform 0.1s ease; transform 0.15s ease;
overflow: hidden; overflow: hidden;
} }
@@ -393,6 +416,13 @@ export class ArtistsView extends LitElement {
* Lifecycle * Lifecycle
* ================================================================ */ * ================================================================ */
override willUpdate(
changed: Map<PropertyKey, unknown>,
) {
super.willUpdate(changed);
this.recomputeArtistCaches();
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.loadCardSize(); this.loadCardSize();
@@ -539,7 +569,7 @@ export class ArtistsView extends LitElement {
const safeIndex = Math.min( const safeIndex = Math.min(
saved, saved,
this.filteredArtists.length - 1, this.cachedFilteredArtists.length - 1,
); );
if (safeIndex <= 0) { if (safeIndex <= 0) {
@@ -724,7 +754,7 @@ export class ArtistsView extends LitElement {
from: number, from: number,
to: number, to: number,
): Set<number> { ): Set<number> {
const filtered = this.filteredArtists; const filtered = this.cachedFilteredArtists;
const start = Math.min(from, to); const start = Math.min(from, to);
const end = Math.max(from, to); const end = Math.max(from, to);
const ids = new Set<number>(); const ids = new Set<number>();
@@ -1282,7 +1312,7 @@ export class ArtistsView extends LitElement {
`; `;
} }
const entries = this.gridEntries; const entries = this.cachedGridEntries;
if (entries.length === 0) { if (entries.length === 0) {
return html` return html`
@@ -261,11 +261,41 @@ export class CoverGrid extends LitElement {
string[] 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 = const term =
this.searchCtrl.term.toLowerCase(); this.searchCtrl.term.toLowerCase();
@@ -448,7 +478,9 @@ export class CoverGrid extends LitElement {
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px;
padding: 5px; padding: 5px;
transition: background-color 0.2s ease; transition:
background-color 0.2s ease,
transform 0.15s ease;
box-sizing: border-box; box-sizing: border-box;
width: var(--card-width, 176px); width: var(--card-width, 176px);
} }
@@ -987,6 +1019,7 @@ export class CoverGrid extends LitElement {
changed: Map<PropertyKey, unknown>, changed: Map<PropertyKey, unknown>,
) { ) {
super.willUpdate(changed); super.willUpdate(changed);
this.recomputeAlbumCache();
// When the parent provides a new external album // When the parent provides a new external album
// list, update local albums and reset selection. // list, update local albums and reset selection.
@@ -1040,7 +1073,7 @@ export class CoverGrid extends LitElement {
// visual position. // visual position.
if (this.expandedAlbumId !== null) { if (this.expandedAlbumId !== null) {
const filtered = const filtered =
this.filteredAlbums; this.cachedFilteredAlbums;
const idx = filtered.findIndex( const idx = filtered.findIndex(
(a) => (a) =>
a.ID === a.ID ===
@@ -1328,7 +1361,7 @@ export class CoverGrid extends LitElement {
this.expandedAlbumId !== null this.expandedAlbumId !== null
) { ) {
const idx = const idx =
this.filteredAlbums.findIndex( this.cachedFilteredAlbums.findIndex(
(a) => (a) =>
a.ID === a.ID ===
this this
@@ -1554,7 +1587,7 @@ export class CoverGrid extends LitElement {
const safeIndex = Math.min( const safeIndex = Math.min(
saved, saved,
this.filteredAlbums.length - 1, this.cachedFilteredAlbums.length - 1,
); );
if (safeIndex <= 0) return; if (safeIndex <= 0) return;
@@ -1773,7 +1806,7 @@ export class CoverGrid extends LitElement {
) { ) {
const pad = CoverGrid.GRID_PADDING; const pad = CoverGrid.GRID_PADDING;
const cols = this.currentColumnCount; const cols = this.currentColumnCount;
const filtered = this.filteredAlbums; const filtered = this.cachedFilteredAlbums;
// Prefer the expanded album as focus. // Prefer the expanded album as focus.
if (this.expandedAlbumId !== null) { if (this.expandedAlbumId !== null) {
@@ -1883,7 +1916,7 @@ export class CoverGrid extends LitElement {
private getCaratOffset(): number { private getCaratOffset(): number {
if (this.expandedAlbumId === null) return 0; if (this.expandedAlbumId === null) return 0;
const idx = this.filteredAlbums.findIndex( const idx = this.cachedFilteredAlbums.findIndex(
(a) => a.ID === this.expandedAlbumId, (a) => a.ID === this.expandedAlbumId,
); );
@@ -1914,7 +1947,7 @@ export class CoverGrid extends LitElement {
* "before" virtualizer; the rest go into "after". * "before" virtualizer; the rest go into "after".
*/ */
private computeSplitIndex() { private computeSplitIndex() {
const filtered = this.filteredAlbums; const filtered = this.cachedFilteredAlbums;
if (this.expandedAlbumId === null) { if (this.expandedAlbumId === null) {
this.splitIndex = filtered.length; this.splitIndex = filtered.length;
@@ -1952,7 +1985,7 @@ export class CoverGrid extends LitElement {
* component state (e.g. selectedAlbums) changes. * component state (e.g. selectedAlbums) changes.
*/ */
private buildGridEntries(): GridEntry[] { private buildGridEntries(): GridEntry[] {
const filtered = this.filteredAlbums; const filtered = this.cachedFilteredAlbums;
const entries: GridEntry[] = []; const entries: GridEntry[] = [];
for (let i = 0; i < filtered.length; i++) { for (let i = 0; i < filtered.length; i++) {
@@ -2239,7 +2272,7 @@ export class CoverGrid extends LitElement {
return; return;
} }
const filtered = this.filteredAlbums; const filtered = this.cachedFilteredAlbums;
const expandedIndex = filtered.findIndex( const expandedIndex = filtered.findIndex(
(a) => a.ID === this.expandedAlbumId, (a) => a.ID === this.expandedAlbumId,
); );
@@ -2346,7 +2379,7 @@ export class CoverGrid extends LitElement {
from: number, from: number,
to: number, to: number,
): Set<number> { ): Set<number> {
const filtered = this.filteredAlbums; const filtered = this.cachedFilteredAlbums;
const start = Math.min(from, to); const start = Math.min(from, to);
const end = Math.max(from, to); const end = Math.max(from, to);
const ids = new Set<number>(); const ids = new Set<number>();
@@ -2574,7 +2607,7 @@ export class CoverGrid extends LitElement {
private syncDropdownToSelection() { private syncDropdownToSelection() {
if (this.selectedAlbums.size === 1) { if (this.selectedAlbums.size === 1) {
const [albumId] = this.selectedAlbums; const [albumId] = this.selectedAlbums;
const album = this.filteredAlbums.find( const album = this.cachedFilteredAlbums.find(
(a) => a.ID === albumId, (a) => a.ID === albumId,
); );
@@ -2599,7 +2632,7 @@ export class CoverGrid extends LitElement {
e: Event, e: Event,
): { album: library.Album; index: number } | null { ): { album: library.Album; index: number } | null {
const path = e.composedPath(); const path = e.composedPath();
const filtered = this.filteredAlbums; const filtered = this.cachedFilteredAlbums;
for (const el of path) { for (const el of path) {
if ( if (
@@ -2775,7 +2808,7 @@ export class CoverGrid extends LitElement {
if (raw === undefined) return; if (raw === undefined) return;
const index = parseInt(raw, 10); const index = parseInt(raw, 10);
const album = this.filteredAlbums[index]; const album = this.cachedFilteredAlbums[index];
if (album && img.src !== album.CoverArtPath) { if (album && img.src !== album.CoverArtPath) {
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` return html`
<div class="empty-state"> <div class="empty-state">
<p>No albums match your search.</p> <p>No albums match your search.</p>
@@ -175,8 +175,40 @@ export class GenresView extends LitElement {
}); });
} }
/** Filtered genres based on search term. */ // -- Memoisation caches for filtered genres --
private get filteredGenres(): Genre[] { 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 = const term =
this.searchCtrl.term.toLowerCase(); 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` static override styles = css`
:host { :host {
display: flex; display: flex;
@@ -227,7 +249,7 @@ export class GenresView extends LitElement {
cursor: pointer; cursor: pointer;
transition: transition:
background-color 0.15s ease, background-color 0.15s ease,
transform 0.1s ease; transform 0.15s ease;
overflow: hidden; overflow: hidden;
} }
@@ -399,6 +421,13 @@ export class GenresView extends LitElement {
* Lifecycle * Lifecycle
* ================================================================ */ * ================================================================ */
override willUpdate(
changed: Map<PropertyKey, unknown>,
) {
super.willUpdate(changed);
this.recomputeGenreCaches();
}
override connectedCallback() { override connectedCallback() {
super.connectedCallback(); super.connectedCallback();
this.loadCardSize(); this.loadCardSize();
@@ -583,7 +612,7 @@ export class GenresView extends LitElement {
const safeIndex = Math.min( const safeIndex = Math.min(
saved, saved,
this.filteredGenres.length - 1, this.cachedFilteredGenres.length - 1,
); );
if (safeIndex <= 0) { if (safeIndex <= 0) {
@@ -768,7 +797,7 @@ export class GenresView extends LitElement {
from: number, from: number,
to: number, to: number,
): Set<string> { ): Set<string> {
const filtered = this.filteredGenres; const filtered = this.cachedFilteredGenres;
const start = Math.min(from, to); const start = Math.min(from, to);
const end = Math.max(from, to); const end = Math.max(from, to);
const names = new Set<string>(); const names = new Set<string>();
@@ -1294,7 +1323,7 @@ export class GenresView extends LitElement {
`; `;
} }
const entries = this.gridEntries; const entries = this.cachedGridEntries;
if (entries.length === 0) { if (entries.length === 0) {
return html` return html`