feat(13-02): add library filter dropdown and wire all views to respect active filter

- Add selectedLibraryId state + ByLibrary conditional calls to library-store
- Add library filter pass-through methods to library-controller
- Create library-filter dropdown component in top bar
- Wire genre-details, cover-grid, artists-view, genres-view to use ByLibrary queries
- Update album-selection helper to respect library filter
- Regenerate Wails bindings for new ByLibrary methods
- Search remains client-side (automatically filtered by loaded data)
- Playlists remain unfiltered (use separate playlist store)
This commit is contained in:
2026-03-16 09:41:27 -04:00
parent 5f7de5060a
commit 42b8cf9f52
16 changed files with 492 additions and 51 deletions
@@ -129,6 +129,22 @@ export class LibraryController implements ReactiveController {
libraryStore.setScrollPosition(view, offset);
}
// ===================================================================
// LIBRARY FILTER
// ===================================================================
get selectedLibraryId(): number | null {
return libraryStore.getSelectedLibraryId();
}
setSelectedLibrary(id: number | null): void {
libraryStore.setSelectedLibrary(id);
}
async getLibraries(): Promise<library.Info[]> {
return libraryStore.getLibraries();
}
// ===================================================================
// COVER SIZE
// ===================================================================
+83 -5
View File
@@ -5,6 +5,12 @@ import {
GetAllArtists,
GetAllGenresWithCounts,
GetAlbumsByArtist,
GetAllTracksByLibrary,
GetAllAlbumsByLibrary,
GetAllArtistsByLibrary,
GetAllGenresWithCountsByLibrary,
GetAlbumsByArtistByLibrary,
GetAllLibrariesWithTrackCounts,
} from '@go/library/Library';
import type { library } from '@go/models';
import { Events } from '../events';
@@ -30,6 +36,9 @@ class LibraryStore {
private albums: library.Album[] | null = null;
private artists: library.Artist[] | null = null;
private genres: library.GenreWithCount[] | null = null;
private libraries: library.Info[] | null = null;
private selectedLibraryIdValue: number | null = null;
private tracksLoading = false;
private albumsLoading = false;
@@ -60,8 +69,19 @@ class LibraryStore {
this.invalidate();
});
EventsOn(Events.LibraryRemoved, () => {
this.libraries = null;
this.invalidate();
});
EventsOn(Events.LibraryAdded, () => {
this.libraries = null;
this.changeGen++;
this.notify();
});
EventsOn(Events.LibraryRenamed, () => {
this.libraries = null;
this.changeGen++;
this.notify();
});
this.loadCoverSize();
this.deferEagerFetch();
@@ -110,7 +130,11 @@ class LibraryStore {
this.notify();
try {
const tracks = await GetAllTracks();
const id = this.selectedLibraryIdValue;
const tracks = id !== null
? await GetAllTracksByLibrary(id)
: await GetAllTracks();
this.tracks = tracks;
this.changeGen++;
@@ -134,7 +158,11 @@ class LibraryStore {
this.notify();
try {
const albums = await GetAllAlbums();
const id = this.selectedLibraryIdValue;
const albums = id !== null
? await GetAllAlbumsByLibrary(id)
: await GetAllAlbums();
this.albums = albums;
this.changeGen++;
@@ -158,7 +186,11 @@ class LibraryStore {
this.notify();
try {
const artists = await GetAllArtists();
const id = this.selectedLibraryIdValue;
const artists = id !== null
? await GetAllArtistsByLibrary(id)
: await GetAllArtists();
this.artists = artists;
this.changeGen++;
@@ -182,7 +214,11 @@ class LibraryStore {
this.notify();
try {
const genres = await GetAllGenresWithCounts();
const id = this.selectedLibraryIdValue;
const genres = id !== null
? await GetAllGenresWithCountsByLibrary(id)
: await GetAllGenresWithCounts();
this.genres = genres;
this.changeGen++;
@@ -196,7 +232,11 @@ class LibraryStore {
async getAlbumsByArtist(
artistID: number,
): Promise<library.Album[]> {
return GetAlbumsByArtist(artistID);
const id = this.selectedLibraryIdValue;
return id !== null
? GetAlbumsByArtistByLibrary(artistID, id)
: GetAlbumsByArtist(artistID);
}
/**
@@ -206,10 +246,20 @@ class LibraryStore {
* result when the all-albums list has already
* been loaded (e.g. the user visited the albums
* view first).
*
* Returns null when a library filter is active
* because the cached albums are already filtered
* by library — the client-side ArtistName match
* is correct but forcing a backend query ensures
* consistency.
*/
getAlbumsByArtistNameCached(
artistName: string,
): library.Album[] | null {
if (this.selectedLibraryIdValue !== null) {
return null;
}
if (this.albums === null) return null;
return this.albums.filter(
@@ -265,6 +315,34 @@ class LibraryStore {
return this.changeGen;
}
// ===================================================================
// LIBRARY FILTER
// ===================================================================
getSelectedLibraryId(): number | null {
return this.selectedLibraryIdValue;
}
setSelectedLibrary(id: number | null): void {
if (id === this.selectedLibraryIdValue) return;
this.selectedLibraryIdValue = id;
this.invalidate();
}
async getLibraries(): Promise<library.Info[]> {
if (this.libraries !== null) {
return this.libraries;
}
const libs =
await GetAllLibrariesWithTrackCounts();
this.libraries = libs;
return libs;
}
// ===================================================================
// SCROLL POSITION
// ===================================================================