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
@@ -13,6 +13,8 @@ import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
import {
GetAlbumsByArtist,
GetAlbumTracks,
GetAlbumsByArtistByLibrary,
GetAlbumTracksByLibrary,
} from '@go/library/Library';
import { library } from '@go/models';
import { LibraryController } from '@store/controllers/library-controller';
@@ -927,20 +929,31 @@ export class ArtistsView
/**
* Fetches all file paths for an artist by
* loading their albums, then each album's
* tracks.
* tracks. Respects the active library filter.
*/
private async getArtistFilePaths(
artist: library.Artist,
): Promise<string[]> {
try {
const albums =
await GetAlbumsByArtist(artist.ID);
const libId =
this.libraryCtrl.selectedLibraryId;
const albums = libId !== null
? await GetAlbumsByArtistByLibrary(
artist.ID,
libId,
)
: await GetAlbumsByArtist(artist.ID);
const allPaths: string[] = [];
for (const album of albums) {
const tracks =
await GetAlbumTracks(album.ID);
const tracks = libId !== null
? await GetAlbumTracksByLibrary(
album.ID,
libId,
)
: await GetAlbumTracks(album.ID);
for (const t of tracks) {
allPaths.push(t.FilePath);
@@ -1,4 +1,8 @@
import { GetAlbumTracks } from '@go/library/Library';
import {
GetAlbumTracks,
GetAlbumTracksByLibrary,
} from '@go/library/Library';
import { libraryStore } from '@store/library-store';
import type { library } from '@go/models';
import type { CoverArtUrls } from '@components/track-details/track-details.js';
@@ -43,6 +47,21 @@ export class AlbumSelectionManager {
this.albumFilePathCache.clear();
}
/**
* Fetch album tracks respecting the active
* library filter.
*/
private fetchAlbumTracks(
albumId: number,
): Promise<library.Track[]> {
const libId =
libraryStore.getSelectedLibraryId();
return libId !== null
? GetAlbumTracksByLibrary(albumId, libId)
: GetAlbumTracks(albumId);
}
// ================================================================
// Album selection helpers
// ================================================================
@@ -134,9 +153,8 @@ export class AlbumSelectionManager {
album: library.Album,
): Promise<string[]> {
try {
const tracks = await GetAlbumTracks(
album.ID,
);
const tracks =
await this.fetchAlbumTracks(album.ID);
return tracks.map((t) => t.FilePath);
} catch (error) {
@@ -174,9 +192,10 @@ export class AlbumSelectionManager {
if (!album) continue;
try {
const tracks = await GetAlbumTracks(
album.ID,
);
const tracks =
await this.fetchAlbumTracks(
album.ID,
);
// Only store if still selected.
if (selectedAlbums.has(album.ID)) {
@@ -11,7 +11,10 @@ import type {
VisibilityChangedEvent,
} from '@lit-labs/virtualizer';
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
import { GetAlbumTracks } from '@go/library/Library';
import {
GetAlbumTracks,
GetAlbumTracksByLibrary,
} from '@go/library/Library';
import { library } from '@go/models';
import { LibraryController } from '@store/controllers/library-controller';
import { SearchController } from '@store/controllers/search-controller';
@@ -973,9 +976,15 @@ export class CoverGrid
this.lastSelectedTrackIndex = null;
try {
const tracks = await GetAlbumTracks(
album.ID,
);
const libId =
this.libraryCtrl.selectedLibraryId;
const tracks = libId !== null
? await GetAlbumTracksByLibrary(
album.ID,
libId,
)
: await GetAlbumTracks(album.ID);
if (this.expandedAlbumId === album.ID) {
this.expandedTracks = tracks;
@@ -5,9 +5,13 @@ import {
state,
} from 'lit/decorators.js';
import { library } from '@go/models';
import { GetTracksByGenre } from '@go/library/Library';
import {
GetTracksByGenre,
GetTracksByGenreByLibrary,
} from '@go/library/Library';
import { EventsOn } from '@runtime/runtime';
import { Events } from '../../events';
import { libraryStore } from '@store/library-store';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@components/track-list/track-list.js';
import { designTokens } from '../../styles/tokens.css';
@@ -176,9 +180,17 @@ export class GenreDetails extends LitElement {
if (!this.genreName) return;
try {
this.tracks = await GetTracksByGenre(
this.genreName,
);
const libId =
libraryStore.getSelectedLibraryId();
this.tracks = libId !== null
? await GetTracksByGenreByLibrary(
this.genreName,
libId,
)
: await GetTracksByGenre(
this.genreName,
);
} catch (error) {
console.error(
'Error loading genre tracks:',
@@ -10,7 +10,10 @@ import type {
VisibilityChangedEvent,
} from '@lit-labs/virtualizer';
import { grid } from '@lit-labs/virtualizer/layouts/grid.js';
import { GetTracksByGenre } from '@go/library/Library';
import {
GetTracksByGenre,
GetTracksByGenreByLibrary,
} from '@go/library/Library';
import type { library } from '@go/models';
import { LibraryController } from '@store/controllers/library-controller';
import { SearchController } from '@store/controllers/search-controller';
@@ -723,16 +726,25 @@ export class GenresView
/**
* Fetch file paths for a set of genre names by
* querying the backend for each genre.
* Respects the active library filter.
*/
private async getFilePathsForGenres(
genreNames: Iterable<string>,
): Promise<string[]> {
const seen = new Set<string>();
const allPaths: string[] = [];
const libId =
this.libraryCtrl.selectedLibraryId;
const promises = Array.from(
genreNames,
(name) => GetTracksByGenre(name),
(name) =>
libId !== null
? GetTracksByGenreByLibrary(
name,
libId,
)
: GetTracksByGenre(name),
);
const results = await Promise.all(promises);
@@ -0,0 +1,107 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { LibraryController } from '@store/controllers/library-controller';
import type { library } from '@go/models';
import { designTokens } from '../../styles/tokens.css';
/**
* Compact dropdown in the top bar for filtering all
* browse views to a specific library. "All Libraries"
* (value = null) shows the unified merged view.
*/
@customElement('library-filter')
export class LibraryFilter extends LitElement {
private libraryCtrl = new LibraryController(this);
@state()
private libraries: library.Info[] = [];
static override styles = [designTokens, css`
:host {
display: flex;
align-items: center;
}
select {
height: 32px;
padding: 0 8px;
border-radius: 6px;
border: 1px solid
var(--yj-border-subtle, #555);
background: var(--yj-bg-surface, #212529);
color: var(--yj-text-primary, #fff);
font-size: var(--yj-text-md);
font-family: inherit;
cursor: pointer;
outline: none;
min-width: 120px;
max-width: 200px;
}
select:focus {
border-color: var(--yj-accent, #ffd43b);
}
option {
background: var(--yj-bg-surface, #212529);
color: var(--yj-text-primary, #fff);
}
`];
override connectedCallback() {
super.connectedCallback();
this.loadLibraries();
}
private async loadLibraries() {
try {
this.libraries =
await this.libraryCtrl.getLibraries();
} catch (error) {
console.error(
'Error loading libraries:',
error,
);
}
}
private handleChange = (e: Event) => {
const select = e.target as HTMLSelectElement;
const value = select.value;
const id = value === ''
? null
: Number(value);
this.libraryCtrl.setSelectedLibrary(id);
};
override render() {
const selected =
this.libraryCtrl.selectedLibraryId;
const selectValue =
selected === null ? '' : String(selected);
return html`
<select
.value=${selectValue}
@change=${this.handleChange}
aria-label="Library filter"
>
<option value="">All Libraries</option>
${this.libraries.map(
(lib) => html`
<option value=${lib.id}>
${lib.name}
</option>
`,
)}
</select>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
'library-filter': LibraryFilter;
}
}