docs(quick-17): plan playlist view subpage refactor

This commit is contained in:
2026-03-07 22:03:11 -05:00
parent 0bd8cefa00
commit 2bdabf0940
@@ -0,0 +1,289 @@
---
phase: quick-17
plan: 1
type: execute
wave: 1
depends_on: []
files_modified:
- frontend/src/components/playlist-details/playlist-details.ts
- frontend/src/components/playlist-view/playlist-view.ts
- frontend/index.ts
autonomous: true
requirements: [QUICK-17]
must_haves:
truths:
- "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"
artifacts:
- path: "frontend/src/components/playlist-details/playlist-details.ts"
provides: "Playlist detail subpage component"
- path: "frontend/src/components/playlist-view/playlist-view.ts"
provides: "Playlist list view (no more inline track expansion)"
- path: "frontend/index.ts"
provides: "Navigation routing for playlist-details"
key_links:
- from: "frontend/src/components/playlist-view/playlist-view.ts"
to: "frontend/index.ts"
via: "CustomEvent('navigate', { view: 'playlist-details', playlistId, playlistName })"
pattern: "navigate.*playlist-details"
- from: "frontend/src/components/playlist-details/playlist-details.ts"
to: "frontend/index.ts"
via: "CustomEvent('navigate', { view: 'playlists' })"
pattern: "navigate.*playlists"
---
<objective>
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.
</objective>
<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>
<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)
<interfaces>
<!-- Navigation pattern from genre-details (the pattern to follow): -->
From frontend/src/components/genre-details/genre-details.ts:
```typescript
// 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:
```typescript
// 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:
```typescript
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:
```typescript
// 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:
```typescript
// 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[] }
```
</interfaces>
</context>
<tasks>
<task type="auto">
<name>Task 1: Create playlist-details component</name>
<files>frontend/src/components/playlist-details/playlist-details.ts, frontend/index.ts</files>
<action>
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;
}
```
</action>
<verify>
`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.
</verify>
<done>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.</done>
</task>
<task type="auto">
<name>Task 2: Simplify playlist-view to navigate instead of expand</name>
<files>frontend/src/components/playlist-view/playlist-view.ts</files>
<action>
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:
```typescript
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.
</action>
<verify>
`npx tsc --noEmit` passes. Verify playlist-view no longer has any `expanded`, `renderPlaylistBody`, `handleTrackClick`, `SelectionController`, or chevron references.
</verify>
<done>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.</done>
</task>
</tasks>
<verification>
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
</verification>
<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>
<output>
After completion, create `.planning/quick/17-refactor-playlist-view-to-use-subpages-l/17-SUMMARY.md`
</output>