Files
yellowjacket/.planning/phases/13-library-views-phantom-tracks/13-02-PLAN.md
T

19 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
13-library-views-phantom-tracks 02 execute 2
13-01
frontend/src/store/library-store.ts
frontend/src/store/controllers/library-controller.ts
frontend/src/components/library-filter/library-filter.ts
frontend/index.html
frontend/index.ts
frontend/src/components/track-list/track-list.ts
frontend/src/components/cover-grid/cover-grid.ts
frontend/src/components/artists-view/artists-view.ts
frontend/src/components/genres-view/genres-view.ts
frontend/src/components/artist-details/artist-details.ts
frontend/src/components/genre-details/genre-details.ts
frontend/src/components/search-bar/search-bar.ts
false
VIEW-01
VIEW-02
VIEW-03
VIEW-04
PLAY-01
PLAY-02
PLAY-03
truths artifacts key_links
Default view shows tracks from all libraries merged (unified presentation)
User can select a specific library from a dropdown in the top bar and all views show only that library's content
Search results respect the active library filter
Switching library filter triggers a backend re-fetch with loading state
Scroll positions reset when switching library filter
Playlists always show all tracks regardless of library filter
Phantom tracks appear with existing phantom styling when a library is removed
Detail views (artist, genre) respect the active library filter
Library filter resets to All Libraries on app restart (no persistence)
path provides min_lines
frontend/src/components/library-filter/library-filter.ts Library filter dropdown component 60
path provides contains
frontend/src/store/library-store.ts selectedLibraryId state + filtered fetch logic selectedLibraryId
path provides contains
frontend/src/store/controllers/library-controller.ts selectedLibraryId getter/setter pass-through selectedLibraryId
path provides contains
frontend/index.html library-filter element in top bar <library-filter>
from to via pattern
frontend/src/store/library-store.ts @go/library/Library GetAllTracksByLibrary / GetAllTracks conditional call GetAllTracksByLibrary|GetAllTracks
from to via pattern
frontend/src/components/library-filter/library-filter.ts frontend/src/store/library-store.ts libraryStore.setSelectedLibrary() setSelectedLibrary
from to via pattern
frontend/src/components/track-list/track-list.ts frontend/src/store/library-store.ts libraryCtrl.getTracks() (now library-aware) getTracks
Add library filter state to the frontend store, a compact dropdown control in the top bar, and wire all browse views + search to respect the active library filter.

Purpose: Users need to filter their entire music collection to a single library or view all merged. This plan adds the filter UI and connects it to all views via the existing store/controller/component pattern. Cross-library playlists and phantom tracks already work via existing infrastructure (Phase 10 schema + Phase 12 CRUD pre-populate phantom metadata + playlist-details phantom rendering) — this plan verifies they work correctly in the multi-library context.

Output: Working library filter dropdown, all views respond to filter changes, search respects filter, playlists remain unfiltered, phantom tracks verified

<execution_context> @/home/caleb/.config/opencode/get-shit-done/workflows/execute-plan.md @/home/caleb/.config/opencode/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/13-library-views-phantom-tracks/13-CONTEXT.md @.planning/phases/13-library-views-phantom-tracks/13-01-SUMMARY.md

From backend/library/query.go — new methods added by Plan 13-01:

func (l *Library) GetAllTracksByLibrary(libraryID int64) ([]Track, error)
func (l *Library) GetAllAlbumsByLibrary(libraryID int64) ([]Album, error)
func (l *Library) GetAllArtistsByLibrary(libraryID int64) ([]Artist, error)
func (l *Library) GetAlbumsByArtistByLibrary(artistID, libraryID int64) ([]Album, error)
func (l *Library) GetAllGenresWithCountsByLibrary(libraryID int64) ([]GenreWithCount, error)
func (l *Library) GetTracksByGenreByLibrary(genreName string, libraryID int64) ([]Track, error)
func (l *Library) GetAlbumTracksByLibrary(albumID, libraryID int64) ([]Track, error)
func (l *Library) SearchTracksByLibrary(query string, libraryID int64) ([]Track, error)
// Existing unfiltered methods remain unchanged

From frontend/src/store/library-store.ts — current state shape:

class LibraryStore {
    private tracks: library.Track[] | null;
    private albums: library.Album[] | null;
    private artists: library.Artist[] | null;
    private genres: library.GenreWithCount[] | null;
    // Loading flags, scroll positions, changeGen, coverSize...
    async getTracks(): Promise<library.Track[]>  // calls GetAllTracks()
    async getAlbums(): Promise<library.Album[]>  // calls GetAllAlbums()
    async getArtists(): Promise<library.Artist[]> // calls GetAllArtists()
    async getGenres(): Promise<library.GenreWithCount[]> // calls GetAllGenresWithCounts()
    async getAlbumsByArtist(id: number): Promise<library.Album[]>
    private invalidate(): void  // nulls caches + changeGen++ + eagerFetch()
}

From frontend/src/store/library-store.ts — existing imports:

import { GetAllTracks, GetAllAlbums, GetAllArtists, GetAllGenresWithCounts, GetAlbumsByArtist } from '@go/library/Library';

After Plan 13-01 + Wails binding regen, these will also be available:

import { GetAllTracksByLibrary, GetAllAlbumsByLibrary, GetAllArtistsByLibrary,
         GetAllGenresWithCountsByLibrary, GetAlbumsByArtistByLibrary,
         GetTracksByGenreByLibrary, GetAlbumTracksByLibrary,
         SearchTracksByLibrary } from '@go/library/Library';

From backend/library/query.go — library info for dropdown:

func (l *Library) GetAllLibrariesWithTrackCounts() ([]Info, error)
// Info struct: { ID int64, Name string, Path string, TrackCount int64 }

Already available as Wails binding:

import { GetAllLibrariesWithTrackCounts } from '@go/library/Library';

From frontend/index.html — top bar structure:

<header class="top-bar">
    <hgroup>
        <h1 class="title">YellowJacket</h1>
        <h3 class="subtitle">Music how it was meant to bee.</h3>
    </hgroup>
    <search-bar></search-bar>
</header>

From frontend/src/components/genre-details/genre-details.ts — direct Wails binding:

import { GetTracksByGenre } from '@go/library/Library';
// calls GetTracksByGenre(this.genreName) directly, bypasses library store

From frontend/src/components/track-list/track-list.ts — search integration:

import { SearchTracks } from '@go/library/Library';
// loadTracks() calls libraryCtrl.getTracks() for browse
// handleSearchResult() calls SearchTracks(term) for search
Task 1: Add library filter state to store + controller, create dropdown component, wire all views frontend/src/store/library-store.ts frontend/src/store/controllers/library-controller.ts frontend/src/components/library-filter/library-filter.ts frontend/index.html frontend/index.ts frontend/src/components/track-list/track-list.ts frontend/src/components/cover-grid/cover-grid.ts frontend/src/components/artists-view/artists-view.ts frontend/src/components/genres-view/genres-view.ts frontend/src/components/artist-details/artist-details.ts frontend/src/components/genre-details/genre-details.ts frontend/src/components/search-bar/search-bar.ts **Step 1: Library store filter state** (`library-store.ts`)

Add a selectedLibraryId: number | null field to LibraryStore (null = "All Libraries"). Add methods:

  • getSelectedLibraryId(): number | null — returns current filter
  • setSelectedLibrary(id: number | null): void — sets filter, calls invalidate() which clears caches, resets scroll positions, and triggers eagerFetch(). The existing invalidation + eager refetch pattern handles everything.
  • getLibraries(): Promise<library.Info[]> — calls GetAllLibrariesWithTrackCounts(). Cache the result in a private libraries: library.Info[] | null field. Invalidate on LibraryAdded, LibraryRenamed, LibraryRemoved events (the last two listeners already exist — extend them).

Modify getTracks(): if selectedLibraryId is not null, call GetAllTracksByLibrary(this.selectedLibraryId) instead of GetAllTracks(). Similarly for getAlbums()GetAllAlbumsByLibrary, getArtists()GetAllArtistsByLibrary, getGenres()GetAllGenresWithCountsByLibrary.

Modify getAlbumsByArtist(artistID): if selectedLibraryId is not null, call GetAlbumsByArtistByLibrary(artistID, this.selectedLibraryId) instead of GetAlbumsByArtist(artistID).

Add imports for the new Wails bindings: GetAllTracksByLibrary, GetAllAlbumsByLibrary, GetAllArtistsByLibrary, GetAllGenresWithCountsByLibrary, GetAlbumsByArtistByLibrary, GetAllLibrariesWithTrackCounts.

Also add getAlbumsByArtistNameCached(): when selectedLibraryId is set, this should return null (force a backend query instead of client-side filtering, since cached albums are already library-filtered).

Step 2: Library controller pass-through (library-controller.ts)

Add pass-through methods:

  • get selectedLibraryId(): number | null
  • setSelectedLibrary(id: number | null): void
  • getLibraries(): Promise<library.Info[]>

Step 3: Library filter dropdown component (NEW file library-filter.ts)

Create frontend/src/components/library-filter/library-filter.ts — a compact <library-filter> Lit component:

  • Uses LibraryController to get library list and current selection
  • Renders as a styled <select> dropdown (native select for simplicity and accessibility):
    • First option: "All Libraries" (value="" or value="0")
    • One option per library: library name (value=library.id)
  • On change: calls libraryStore.setSelectedLibrary(id) (null for "All Libraries", numeric ID otherwise)
  • Loads library list on connectedCallback via libraryCtrl.getLibraries()
  • Refreshes library list on LibraryAdded/LibraryRemoved events (the store handles this — controller just needs to re-read)
  • Styling: matches existing top bar aesthetic with design tokens — var(--yj-bg-surface) background, var(--yj-text-primary) text, var(--yj-border-subtle) border, var(--yj-accent) focus ring. Compact height matching search bar (32px). No animation per CONTEXT.md (Claude's discretion — keep it simple).
  • Register in HTMLElementTagNameMap

Step 4: Wire into index.html and index.ts

In frontend/index.html: add <library-filter></library-filter> in the <header class="top-bar"> between the <hgroup> and <search-bar>:

<header class="top-bar">
    <hgroup>...</hgroup>
    <library-filter></library-filter>
    <search-bar></search-bar>
</header>

In frontend/index.ts: add import for the new component:

import '@components/library-filter/library-filter.js';

Step 5: Wire search to respect library filter (track-list.ts)

The track-list component has a handleSearchResult method that calls SearchTracks(term). Modify this:

  • Import SearchTracksByLibrary from Wails bindings
  • When selectedLibraryId is set on the library controller, call SearchTracksByLibrary(term, selectedLibraryId) instead of SearchTracks(term)
  • Access the library filter via the existing libraryCtrl instance

Find the search-related code in track-list.ts and update accordingly. The search bar itself doesn't need changes — it just sets the search term. The track-list reacts to term changes and performs the actual search.

Step 6: Wire genre-details and artist-details to respect library filter

genre-details.ts calls GetTracksByGenre(genreName) directly (bypasses store). Modify:

  • Import GetTracksByGenreByLibrary from Wails bindings
  • Import libraryStore (or use a LibraryController)
  • When selectedLibraryId is set, call GetTracksByGenreByLibrary(genreName, selectedLibraryId) instead

artist-details.ts calls libraryCtrl.getAlbumsByArtist(id) which goes through the store — this is already handled by Step 1's store changes.

cover-grid.ts — album track expansion dropdown calls GetAlbumTracks(albumID) directly. Import GetAlbumTracksByLibrary and use it when filter is active. Check if cover-grid.ts has a direct GetAlbumTracks import and update it.

Step 7: Ensure playlists remain unfiltered

Verify that playlist-view and playlist-details do NOT use LibraryController or libraryStore for their data. They should use PlaylistStore / direct Wails bindings to playlist.Service — which is library-agnostic. No changes needed if confirmed.

Step 8: Queue context — playing from filtered view

Per CONTEXT.md locked decision: "Queue matches the filter context — playing from a filtered view populates the queue with only that library's tracks."

This already works naturally because:

  • When library filter is active, track-list.tracks contains only filtered tracks
  • Double-click sends that track's FilePath to queueStore.setQueue([filePath], 0)
  • Context menu "Play" sends selected (filtered) file paths to queueStore.setQueue(filePaths, 0, true)
  • The queue service resolves tracks from file paths in the DB — which includes tracks from all libraries

However, there's a subtlety: the Queue's SetQueue on the backend resolves track metadata by file path from the DB. Since files from other libraries still exist in the DB, this works correctly. The queue will contain whatever file paths were sent from the filtered view.

No code changes needed for queue — the filtering happens at the data source (library store → track-list), and queue just receives file paths.

Verify build compiles: cd frontend && npm run check (TypeScript check) and test the app with wails dev -tags webkit2_41.

Important conventions:

  • Use override keyword on all Lit lifecycle methods
  • Use import type for type-only imports (verbatimModuleSyntax)
  • Use design tokens from ../../styles/tokens.css (import designTokens)
  • Arrow function event handlers (auto-bound this)
  • Register component in HTMLElementTagNameMap
  • Lines under 100 chars where possible cd /mnt/vault/dev/golang/yellowjacket && wails build -tags webkit2_41 Library filter dropdown appears in top bar, shows "All Libraries" by default plus all configured libraries. Selecting a library causes all browse views (tracks, albums, artists, genres) to show only that library's content. Search respects the filter. Detail views (artist-details, genre-details) respect the filter. Playlists remain unfiltered. Scroll positions reset on filter change. Filter resets on app restart.
Task 2: Verify library filter, cross-library playlists, and phantom tracks end-to-end Run the app with `wails dev -tags webkit2_41` and verify all Phase 13 requirements:

1. Library filter dropdown (VIEW-02)

  • Compact dropdown appears in top bar between title and search bar
  • Shows "All Libraries" as default selection
  • Lists all configured libraries by name
  • Selecting a library immediately refreshes all views

2. Unified view — All Libraries (VIEW-01)

  • With "All Libraries" selected, track list shows tracks from ALL libraries
  • Albums view shows albums from all libraries
  • Artists view shows artists from all libraries
  • Genres view shows genres from all libraries

3. Filtered view — specific library (VIEW-02, VIEW-03)

  • Selecting a specific library shows only that library's tracks
  • Albums view shows only albums with tracks in selected library
  • Artists view shows only artists with albums in selected library
  • Genres view shows only genres with tracks in selected library
  • Artist detail page (click an artist) shows only that artist's albums in selected library
  • Genre detail page (click a genre) shows only that genre's tracks in selected library

4. Search with library filter (VIEW-04)

  • With "All Libraries" selected, search returns results from all libraries
  • With a specific library selected, search returns only matches from that library

5. Cross-library playlists (PLAY-01)

  • Create a playlist and add tracks from different libraries — they all appear correctly
  • Playlist view is NOT affected by library filter (shows all playlists always)
  • Playlist detail view shows ALL tracks regardless of active library filter

6. Phantom tracks (PLAY-02, PLAY-03)

  • Remove a library that has tracks in a playlist
  • Those tracks become phantom entries (greyed out with warning icon)
  • Phantom tracks show preserved title, artist, album metadata
  • Phantom resolver (locate/remove buttons) works on the phantom entries

7. UX details

  • Scroll positions reset when switching library filter
  • Loading skeleton shows briefly during filter switch
  • Filter resets to "All Libraries" on app restart
  • Queue plays correctly when tracks are from filtered view Human verification — all checklist items above pass All 7 requirement groups verified: VIEW-01 (unified), VIEW-02 (filtered), VIEW-03 (browse views filtered), VIEW-04 (search filtered), PLAY-01 (cross-library playlists), PLAY-02 (phantom preservation), PLAY-03 (phantom display)
- `wails build -tags webkit2_41` completes successfully - Library filter dropdown renders in top bar - All 7 requirements verified: VIEW-01 (unified), VIEW-02 (filtered), VIEW-03 (browse filtered), VIEW-04 (search filtered), PLAY-01 (cross-library playlists), PLAY-02 (phantom preservation), PLAY-03 (phantom display) - No regression in existing functionality (playlists, queue, playback)

<success_criteria>

  • Library filter dropdown in top bar with "All Libraries" default + per-library options
  • Track, album, artist, genre views all filter by selected library
  • Search respects active library filter
  • Playlists remain unfiltered (cross-library by design)
  • Phantom tracks display correctly after library removal
  • Queue populated from filtered context
  • Human checkpoint passed </success_criteria>
After completion, create `.planning/phases/13-library-views-phantom-tracks/13-02-SUMMARY.md`