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();
}
// ===================================================================