diff --git a/frontend/index.html b/frontend/index.html index b727352..f63921a 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6,8 +6,8 @@ yellowjacket + -
diff --git a/frontend/index.ts b/frontend/index.ts index cbbe0af..aaf28d7 100644 --- a/frontend/index.ts +++ b/frontend/index.ts @@ -17,8 +17,6 @@ import type { SearchBar } from '@components/search-bar/search-bar.ts'; import '@awesome.me/webawesome/dist/styles/themes/default.css'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import { setBasePath } from '@awesome.me/webawesome/dist/webawesome.js'; -import { libraryStore } from '@store/library-store'; -import { playlistStore } from '@store/playlist-store'; import { queueStore } from '@store/queue-store'; import { searchStore } from '@store/search-store'; // Importing the theme store triggers initialization: it fetches the saved @@ -32,12 +30,6 @@ import type { DragActiveDetail } from '@utils/drag-controller'; setBasePath('/dist/webawesome'); -// Pre-fetch data for views not yet mounted so they're cached when navigated to. -// These are fire-and-forget — the singleton stores deduplicate concurrent fetches, -// so if a component mounts before this completes, it joins the in-flight request. -libraryStore.getAlbums(); -playlistStore.getPlaylists(); - // Navigation event listener for view switching document.addEventListener('navigate', (e: Event) => { const { view } = (e as CustomEvent).detail; diff --git a/frontend/src/components/artists-view/artists-view.ts b/frontend/src/components/artists-view/artists-view.ts index 707596d..cbfb776 100644 --- a/frontend/src/components/artists-view/artists-view.ts +++ b/frontend/src/components/artists-view/artists-view.ts @@ -23,6 +23,7 @@ import { contextMenuStyles, } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; + import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; @@ -360,6 +361,7 @@ export class ArtistsView ); font-size: 14px; } + `, ]; diff --git a/frontend/src/components/genres-view/genres-view.ts b/frontend/src/components/genres-view/genres-view.ts index 9aaec39..5467e52 100644 --- a/frontend/src/components/genres-view/genres-view.ts +++ b/frontend/src/components/genres-view/genres-view.ts @@ -10,12 +10,8 @@ import type { VisibilityChangedEvent, } from '@lit-labs/virtualizer'; import { grid } from '@lit-labs/virtualizer/layouts/grid.js'; -import { - GetAllGenresWithCounts, - GetTracksByGenre, -} from '@go/library/Library'; -import { EventsOn } from '@runtime/runtime'; -import { Events } from '../../events'; +import { GetTracksByGenre } from '@go/library/Library'; +import type { library } from '@go/models'; import { LibraryController } from '@store/controllers/library-controller'; import { SearchController } from '@store/controllers/search-controller'; import { queueStore } from '@store/queue-store'; @@ -24,6 +20,7 @@ import { contextMenuStyles, } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; + import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; @@ -66,8 +63,11 @@ export class GenresView private ctxMenu = new ContextMenuController(this); private wheelListenerAttached = false; private lastSearchTerm = ''; - private scanCompleteCleanup: (() => void) | null = - null; + + /** Tracks the store's cached array reference to detect refreshes. */ + private lastGenresRef: + | library.GenreWithCount[] + | null = null; private scrollDebounceTimer: ReturnType< typeof setTimeout @@ -379,22 +379,12 @@ export class GenresView super.connectedCallback(); this.loadCardSize(); this.loadGenres(); - - this.scanCompleteCleanup = EventsOn( - Events.LibraryScanComplete, - () => this.loadGenres(), - ); } override disconnectedCallback() { super.disconnectedCallback(); this.detachWheelListener(); - if (this.scanCompleteCleanup) { - this.scanCompleteCleanup(); - this.scanCompleteCleanup = null; - } - if (this.scrollDebounceTimer !== null) { clearTimeout(this.scrollDebounceTimer); } @@ -412,6 +402,19 @@ export class GenresView this.lastSearchTerm = currentTerm; this.clearSelection(); } + + // Re-fetch when the store delivers fresh + // data after eager refetch on invalidation. + const cached = + this.libraryCtrl.cachedGenres; + + if ( + cached !== null && + cached !== this.lastGenresRef + ) { + this.lastGenresRef = cached; + this.loadGenres(); + } } /* ================================================================ @@ -423,7 +426,7 @@ export class GenresView this.loading = true; const rows = - await GetAllGenresWithCounts(); + await this.libraryCtrl.getGenres(); this.genres = (rows ?? []).map((r) => ({ name: r.Name, diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index 74ea87a..c498644 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -12,6 +12,7 @@ import { ContextMenuController, contextMenuStyles, } from '@utils/context-menu-controller.js'; + import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import { PlayerController } from '@store/controllers/player-controller'; import { SearchController } from '@store/controllers/search-controller'; @@ -728,6 +729,7 @@ export class TrackList extends LitElement implements SelectionHost, ContextMenuH user-select: none; } + .sort-anchor { display: inline-flex; align-items: center; diff --git a/frontend/src/store/controllers/library-controller.ts b/frontend/src/store/controllers/library-controller.ts index ea2a38f..9d50122 100644 --- a/frontend/src/store/controllers/library-controller.ts +++ b/frontend/src/store/controllers/library-controller.ts @@ -55,6 +55,10 @@ export class LibraryController implements ReactiveController { return libraryStore.getArtists(); } + async getGenres(): Promise { + return libraryStore.getGenres(); + } + async getAlbumsByArtist( artistID: number, ): Promise { @@ -81,6 +85,10 @@ export class LibraryController implements ReactiveController { return libraryStore.getCachedArtists(); } + get cachedGenres(): library.GenreWithCount[] | null { + return libraryStore.getCachedGenres(); + } + get tracksLoading(): boolean { return libraryStore.isTracksLoading(); } @@ -93,6 +101,10 @@ export class LibraryController implements ReactiveController { return libraryStore.isArtistsLoading(); } + get genresLoading(): boolean { + return libraryStore.isGenresLoading(); + } + // =================================================================== // SCROLL POSITION // =================================================================== diff --git a/frontend/src/store/library-store.ts b/frontend/src/store/library-store.ts index 2449d05..e68fd63 100644 --- a/frontend/src/store/library-store.ts +++ b/frontend/src/store/library-store.ts @@ -3,6 +3,7 @@ import { GetAllTracks, GetAllAlbums, GetAllArtists, + GetAllGenresWithCounts, GetAlbumsByArtist, } from '@go/library/Library'; import type { library } from '@go/models'; @@ -28,10 +29,12 @@ class LibraryStore { private tracks: library.Track[] | null = null; private albums: library.Album[] | null = null; private artists: library.Artist[] | null = null; + private genres: library.GenreWithCount[] | null = null; private tracksLoading = false; private albumsLoading = false; private artistsLoading = false; + private genresLoading = false; private coverSizeValue: number = COVER_SIZE_DEFAULT; @@ -50,6 +53,7 @@ class LibraryStore { }); this.loadCoverSize(); + this.eagerFetch(); } // =================================================================== @@ -126,6 +130,29 @@ class LibraryStore { } } + async getGenres(): Promise { + if (this.genres !== null) { + return this.genres; + } + + if (this.genresLoading) { + return this.waitForGenres(); + } + + this.genresLoading = true; + this.notify(); + + try { + const genres = await GetAllGenresWithCounts(); + this.genres = genres; + + return genres; + } finally { + this.genresLoading = false; + this.notify(); + } + } + async getAlbumsByArtist( artistID: number, ): Promise { @@ -179,6 +206,14 @@ class LibraryStore { return this.artistsLoading; } + getCachedGenres(): library.GenreWithCount[] | null { + return this.genres; + } + + isGenresLoading(): boolean { + return this.genresLoading; + } + // =================================================================== // SCROLL POSITION // =================================================================== @@ -249,21 +284,24 @@ class LibraryStore { this.tracks = null; this.albums = null; this.artists = null; + this.genres = null; this.scrollPositions = { tracks: 0, albums: 0, artists: 0, genres: 0 }; this.notify(); - this.eagerRefetch(); + this.eagerFetch(); } /** - * Re-fetches all data after cache invalidation so that + * Fetches all library data. Called from the constructor + * (initial load) and after cache invalidation so that * controller subscribers receive fresh data on the next * requestUpdate() cycle without needing their own * LibraryScanComplete listener. */ - private eagerRefetch(): void { + private eagerFetch(): void { void this.getTracks(); void this.getAlbums(); void this.getArtists(); + void this.getGenres(); } // =================================================================== @@ -317,6 +355,17 @@ class LibraryStore { }); }); } + + private waitForGenres(): Promise { + return new Promise((resolve) => { + const unsub = this.subscribe(() => { + if (!this.genresLoading && this.genres !== null) { + unsub(); + resolve(this.genres); + } + }); + }); + } } // Singleton instance. diff --git a/frontend/src/store/playlist-store.ts b/frontend/src/store/playlist-store.ts index 4987ee0..0d1523b 100644 --- a/frontend/src/store/playlist-store.ts +++ b/frontend/src/store/playlist-store.ts @@ -35,6 +35,8 @@ class PlaylistStore { EventsOn(Events.PlaylistsRestored, () => { this.invalidate(); }); + + void this.getPlaylists(); } // ===================================================================