Files
yellowjacket/.planning/quick/17-refactor-playlist-view-to-use-subpages-l/17-PLAN.md
T

16 KiB
Raw Blame History

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
quick-17 1 execute 1
frontend/src/components/playlist-details/playlist-details.ts
frontend/src/components/playlist-view/playlist-view.ts
frontend/index.ts
true
QUICK-17
truths artifacts key_links
Clicking a playlist in playlist-view navigates to a dedicated playlist-details subpage
Playlist-details shows a header with back button, playlist icon, playlist name, and track count
Playlist-details shows the full track list for that playlist with all existing interactions (click, dblclick, context menu, drag, phantom handling)
Back button in playlist-details navigates back to the playlist list
Playlist-view no longer expands/collapses playlists inline
path provides
frontend/src/components/playlist-details/playlist-details.ts Playlist detail subpage component
path provides
frontend/src/components/playlist-view/playlist-view.ts Playlist list view (no more inline track expansion)
path provides
frontend/index.ts Navigation routing for playlist-details
from to via pattern
frontend/src/components/playlist-view/playlist-view.ts frontend/index.ts CustomEvent('navigate', { view: 'playlist-details', playlistId, playlistName }) navigate.*playlist-details
from to via pattern
frontend/src/components/playlist-details/playlist-details.ts frontend/index.ts CustomEvent('navigate', { view: 'playlists' }) navigate.*playlists
Refactor the playlist view from an expand/collapse dropdown pattern to a subpage navigation pattern, matching how genre-details and artist-details work. Clicking a playlist navigates to a dedicated playlist-details page with a header (back button + playlist icon + title + track count) and the full track list below.

Purpose: Consistent navigation UX across playlists, genres, and artists — all use the subpage pattern. Output: New playlist-details component, simplified playlist-view, updated navigation routing.

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

@frontend/src/components/genre-details/genre-details.ts (reference pattern — header + back button + content) @frontend/src/components/genres-view/genres-view.ts (reference pattern — click navigates to details) @frontend/src/components/playlist-view/playlist-view.ts (source — refactor this) @frontend/index.ts (navigation routing — add playlist-details case) @frontend/wailsjs/go/playlist/Service.d.ts (available backend APIs) @frontend/src/store/playlist-store.ts (playlist data store) @frontend/src/store/search-store.ts (search store — update SEARCHABLE_VIEWS if needed)

From frontend/src/components/genre-details/genre-details.ts:

// Header: back-button → avatar → genre-info (title + track count)
// Content: <track-list .externalTracks=${this.tracks}>
// Back navigation: dispatches CustomEvent('navigate', { view: 'genres' })

From frontend/index.ts — navigation handler:

// Genre details routing (line 73-82):
case 'genre-details': {
    const { genreName } = (e as CustomEvent).detail;
    const genreEl = document.createElement('genre-details');
    genreEl.setAttribute('genre-name', genreName);
    mainContent.innerHTML = '';
    mainContent.appendChild(genreEl);
    break;
}

From frontend/wailsjs/go/playlist/Service.d.ts:

export function GetPlaylistTracks(arg1:number):Promise<Array<playlist.Track>>;
export function GetAllPlaylistsWithTracks():Promise<Array<playlist.WithTracks>>;
export function AddTracksToPlaylist(arg1:number,arg2:Array<string>):Promise<void>;
export function RemoveTracksFromPlaylist(arg1:number,arg2:Array<number>):Promise<void>;
export function RenamePlaylist(arg1:number,arg2:string):Promise<void>;
export function DeletePlaylist(arg1:number):Promise<void>;
export function RemovePhantomTracks(arg1:number,arg2:Array<string>):Promise<void>;
export function FindDuplicateTracksInPlaylist(arg1:number,arg2:Array<string>):Promise<playlist.DuplicateCheckResult>;

From frontend/src/components/playlist-view/playlist-view.ts:

// PlaylistEntry = { summary: playlist.Summary, expanded: boolean, tracks: playlist.Track[] }
// SelectionHost + ContextMenuHost interfaces
// Uses: PlayerController, PlaylistController, SearchController, SelectionController, ContextMenuController, FavoritesController
// Track interactions: click (select), dblclick (play), contextmenu, drag source, phantom handling
// Playlist-level: context menu (rename, delete, set-default), drag target, multi-select, sort

From frontend/wailsjs/go/models.ts:

// playlist.Summary: { ID: number, Name: string, CreatedAt: string, UpdatedAt: string }
// playlist.Track: { ID, Position, FilePath, Title, Artist, Album, CoverArtPath, CoverArtSmall, CoverArtMedium, CoverArtLarge, Duration, Phantom }
// playlist.WithTracks: { Summary: Summary, Tracks: Track[] }
Task 1: Create playlist-details component frontend/src/components/playlist-details/playlist-details.ts, frontend/index.ts Create `frontend/src/components/playlist-details/playlist-details.ts` — a new LitElement component modeled closely on `genre-details.ts` but with all the track interaction capabilities currently in `playlist-view`'s `renderPlaylistBody`.

Component structure:

  1. Properties: playlistId (Number attribute), playlistName (String attribute).

  2. Header (same layout as genre-details):

    • Back button (dispatches navigate with { view: 'playlists' })
    • Playlist avatar: 80×80 rounded square with list icon (wa-icon name="list") centered inside, using the same gradient background as genre-details' .genre-avatar
    • Playlist info: h1 title + track count subtitle
  3. Content area: Render playlist tracks using the same track-item rendering pattern currently in playlist-view's renderPlaylistBody method. This includes:

    • "Play All" button at the top
    • Track list with <track-info> for normal tracks and phantom row rendering for phantom tracks
    • All track interactions: click (selection), dblclick (play from playlist), right-click context menu, drag source
    • Phantom track interactions: click (select), right-click context menu, locate button, remove button
    • Active track highlighting (uses PlayerController)
    • Track selection (uses SelectionController — this component is the SelectionHost)
  4. Context menus: Move the track-level context menu (play, add to queue, play next, remove from playlist, add to playlist submenu, favorites toggle, track details) and the playlist submenu from playlist-view into this component. Include <track-details>, <phantom-resolver>, and <duplicate-tracks-dialog> elements.

  5. Data loading:

    • On connectedCallback, call GetPlaylistTracks(this.playlistId) to load tracks
    • Listen for Events.PlaylistTracksChanged and Events.PlaylistDeleted to refresh/navigate back
    • Implement a refreshTracks() method that re-fetches tracks after mutations
  6. Drag target: Support dropping tracks onto this playlist (from queue, track-list, or other playlists). Use the same AddTracksToPlaylist + FindDuplicateTracksInPlaylist flow currently in playlist-view's onPlaylistDrop.

  7. Drag source: Support dragging tracks from this playlist to queue or other destinations. Same pattern as playlist-view's onTrackDragStart/onTrackDragEnd.

  8. Search integration: Wire up SearchController. When search term is active, filter visible tracks (same logic as playlist-view's getVisibleTracks). Add playlist-details to SEARCHABLE_VIEWS in search-store.ts.

  9. Styles: Use designTokens mixin. Copy the relevant styles from genre-details for the header section (.back-button, avatar, title, track-count). Copy track-item styles (.track-item, .track-item.active, .track-item.selected, .phantom-row, etc.) and context menu styles from playlist-view. Include .play-all-button styles.

  10. Select-all support: Listen for shortcut:select-all event (same as playlist-view does).

In frontend/index.ts:

  • Add import '@components/playlist-details/playlist-details.ts'; at the top with the other imports
  • Add a case 'playlist-details': block in the navigation switch, following the same pattern as genre-details:
    case 'playlist-details': {
        const { playlistId, playlistName } = (e as CustomEvent).detail;
        const el = document.createElement('playlist-details');
        el.setAttribute('playlist-id', String(playlistId));
        el.setAttribute('playlist-name', playlistName);
        mainContent.innerHTML = '';
        mainContent.appendChild(el);
        break;
    }
    

npx tsc --noEmit passes. The new component file exists with @customElement('playlist-details'), has back button, header, track rendering, context menus, drag support, and the index.ts routing case exists. playlist-details component renders a header with back button + playlist icon + title + track count, displays playlist tracks with all interactions (select, play, context menu, drag, phantom handling), and navigating to playlist-details view works via index.ts routing.

Task 2: Simplify playlist-view to navigate instead of expand frontend/src/components/playlist-view/playlist-view.ts Refactor `playlist-view.ts` to remove all inline track expansion and instead navigate to `playlist-details` on click. This is a significant simplification:

Remove from playlist-view:

  1. The expanded field from PlaylistEntry interface (no longer needed — set type to just { summary: playlist.Summary; tracks: playlist.Track[] }, keep tracks for count display and drag-drop track resolution)
  2. The renderPlaylistBody method entirely
  3. The chevron icon in renderPlaylistItem (no more expand/collapse)
  4. All track-level interaction handlers: handleTrackClick, handleTrackDblClick, handleTrackContextMenu, onTrackDragStart, onTrackDragEnd, ensureSelectionScope, getSelectedTrackIDs, getSelectedFilePaths, removeSelectedTracks, removeSelectedPhantoms, handlePhantomClick, handlePhantomContextMenu, openPhantomResolver, removePhantomTrack
  5. The SelectionController (no more track selection in the list view) and SelectionHost implementation (getItemKey, getItemCount, onSelectionChanged)
  6. The ContextMenuController and ContextMenuHost implementation, and all track-level context menu rendering (the #context-menu popup, #playlist-submenu popup)
  7. Remove activePlaylistIndex state
  8. The <track-details>, <phantom-resolver>, and <duplicate-tracks-dialog> elements from the render method (they move to playlist-details)
  9. The isActiveTrack, isPhantomSelection, resolvePlaylistCoverArt, openTrackDetails methods
  10. The getVisibleTracks and filteredEntries search-related track filtering (search on the list page can just filter playlist names)
  11. Remove SelectionHost and ContextMenuHost from the class declaration
  12. Remove imports that are no longer needed: SelectionController, track-info, track-details, phantom-resolver, duplicate-tracks-dialog, etc.
  13. Remove the clearSelectionHandler and handleSelectAll handlers since track selection is gone
  14. Remove the search-triggered auto-expand logic in updated() (the part that sets expanded: true based on track matches)

Keep in playlist-view:

  1. Playlist list rendering (the grid of playlist items with name + track count)
  2. Playlist-level context menu (rename, delete, set-default) — the #playlist-context-menu popup
  3. Playlist-level selection (Ctrl/Shift+Click for multi-select of playlists for bulk delete)
  4. Create playlist functionality (new playlist button, create form)
  5. Import playlist button
  6. Sort toolbar (sort by name, created, modified, tracks)
  7. Drag-and-drop TARGET: dropping tracks onto a playlist item to add them (keep onPlaylistDragOver, onPlaylistDragLeave, onPlaylistDrop). Also keep empty zone drop and new-button drop.
  8. Search filtering (but simplified to just filter by playlist name, not tracks)
  9. Scroll position persistence
  10. PlaylistController for data loading

Modify handlePlaylistHeaderClick:

  • Remove the expand/collapse toggle behavior for plain clicks
  • Instead, plain click dispatches navigation:
    this.dispatchEvent(
        new CustomEvent('navigate', {
            bubbles: true,
            composed: true,
            detail: {
                view: 'playlist-details',
                playlistId: entry.summary.ID,
                playlistName: entry.summary.Name,
            },
        }),
    );
    
  • Keep Ctrl+Click and Shift+Click for playlist multi-selection (same as before)

Modify renderPlaylistItem:

  • Remove the chevron icon
  • Remove the ${entry.expanded ? this.renderPlaylistBody(...) : nothing} conditional
  • The playlist-header row now just shows: playlist-icon (if favorites) + playlist-name + track-count
  • Keep the right-click context menu handler on the header
  • Keep the drag-over styling for drop targets

Simplify filteredEntries:

  • Only filter by playlist name (remove the track title/artist matching since tracks aren't shown inline)

Remove styles that are no longer needed:

  • .chevron, .chevron.expanded
  • .playlist-body
  • .playlist-actions, .play-all-button
  • .track-item and all its variants (.active, .selected, .phantom)
  • .phantom-row, .phantom-caution, .phantom-path, .phantom-actions, .phantom-icon-btn
  • .tracks-empty
  • Context menu styles for track-level menus

Update loadPlaylists/refreshPlaylists:

  • Remove expanded from the mapped entries

Remove the contextMenuStyles import if no longer needed (check if playlist-level context menu uses it — it does, so keep it).

Note: The PlaylistController, PlayerController, SearchController, FavoritesController may still be needed. Keep FavoritesController for the favorites icon display. Remove PlayerController since active track highlighting is gone from the list. Keep SearchController for name-based search. Keep PlaylistController for data. npx tsc --noEmit passes. Verify playlist-view no longer has any expanded, renderPlaylistBody, handleTrackClick, SelectionController, or chevron references. Playlist-view shows a clean list of playlists without inline track expansion. Plain-clicking a playlist navigates to playlist-details. Ctrl/Shift+Click still multi-selects. Right-click context menu still works for rename/delete/set-default. Drag-drop onto playlists still works. Create/import still works. Sort still works. Search filters by playlist name only.

1. `npx tsc --noEmit` — TypeScript compilation passes with no errors 2. Navigate to Playlists view — shows list of playlists without chevrons or expandable sections 3. Click a playlist — navigates to playlist-details with header (back button, icon, title, track count) and track list 4. Click back button — returns to playlist list 5. Double-click a track in playlist-details — plays the track 6. Right-click track in playlist-details — context menu works (play, queue, remove, etc.) 7. Right-click playlist in list — context menu works (rename, delete, set-default) 8. Ctrl+Click playlists in list — multi-select works for bulk delete 9. Drag tracks from another view onto a playlist in the list — adds tracks 10. Drag tracks from playlist-details to queue — works 11. Create new playlist — form still works 12. Import playlist — button still works 13. Sort playlists — toolbar still works 14. Search — filters playlist names in list view, filters tracks in detail view

<success_criteria>

  • Playlist navigation matches genre/artist pattern: list view → click → detail subpage → back button
  • No inline track expansion/collapse in playlist-view
  • All existing track interactions work in playlist-details (play, select, context menu, drag, phantom handling)
  • All existing playlist management works in playlist-view (create, import, rename, delete, sort, drag-drop target)
  • TypeScript compiles without errors </success_criteria>
After completion, create `.planning/quick/17-refactor-playlist-view-to-use-subpages-l/17-SUMMARY.md`