|
| 1 | +# Now Playing Context Menu Implementation Guide |
| 2 | + |
| 3 | +## Overview |
| 4 | +Task-304 requires adding right-click context menu support to tracks in the Now Playing / Up Next queue list. The context menu should reuse existing library view logic with proper viewport-aware positioning. |
| 5 | + |
| 6 | +## Architecture |
| 7 | + |
| 8 | +### 1. Context Menu Implementation in Library View |
| 9 | + |
| 10 | +#### HTML Template (`app/frontend/views/library.html` lines 330-395) |
| 11 | +- Track context menu div with classes: `context-menu track-context-menu bg-card` |
| 12 | +- Positioned using `:style="contextMenu ? left: ${contextMenu.x}px; top: ${contextMenu.y}px : ''"` |
| 13 | +- Uses `@click.outside` to dismiss menu |
| 14 | +- Renders menu items from `contextMenu.items` array |
| 15 | +- Playlist submenu rendered separately with `getSubmenuStyle()` |
| 16 | + |
| 17 | +#### CSS Styles (`app/frontend/styles.css` lines 579-630) |
| 18 | +- `.context-menu`: fixed positioning, z-index 100, min-width 180px, borders, shadows, padding 0.25rem |
| 19 | +- `.context-menu-item`: flex layout, gap 0.5rem, padding 0.5rem 0.75rem, cursor pointer |
| 20 | +- `.context-menu-item:hover`: bg #dfdfdf |
| 21 | +- `.context-menu-item.disabled`: opacity 0.5, pointer-events none |
| 22 | +- `.context-menu-item.danger`: color destructive |
| 23 | +- `.context-menu-separator`: height 1px background border |
| 24 | + |
| 25 | +#### Mixin: `single-track-context-menu.js` |
| 26 | +Located at `app/frontend/js/mixins/single-track-context-menu.js` |
| 27 | + |
| 28 | +Key properties: |
| 29 | +- `contextMenu`: { x, y, track, items } |
| 30 | +- `playlists`: array for submenu |
| 31 | +- `showPlaylistSubmenu`: boolean to toggle submenu |
| 32 | +- `submenuOnLeft`: boolean for flipped positioning |
| 33 | +- `submenuCloseTimeout`: for delayed submenu close |
| 34 | + |
| 35 | +Key methods: |
| 36 | +- `handleContextMenu(event, track)`: Opens context menu |
| 37 | + - Calculates menu position with viewport bounds checking |
| 38 | + - Determines if submenu should flip left: `x + menuWidth + 45 + submenuWidth > window.innerWidth` |
| 39 | + - Creates menu items array with actions |
| 40 | + - Asynchronously checks favorite status |
| 41 | + |
| 42 | +- `_ctxPlayTrack(track)`: Clear queue, add track, play |
| 43 | +- `_ctxAddToQueue(track)`: Add to end of queue |
| 44 | +- `_ctxPlayNext(track)`: Insert at currentIndex + 1 |
| 45 | +- `_ctxToggleFavorite(track)`: Toggle favorite status |
| 46 | +- `addToPlaylist(playlistId)`: Add track to playlist |
| 47 | +- `_ctxShowInFinder(track)`: Reveal in Finder |
| 48 | + |
| 49 | +#### Mixin: `context-menu-actions.js` (more complex version) |
| 50 | +Located at `app/frontend/js/mixins/context-menu-actions.js` |
| 51 | + |
| 52 | +Used in library-browser for multi-track selections. More sophisticated approach: |
| 53 | +- Handles single and multiple selection |
| 54 | +- More menu items including "Go to Album", "Go to Artist", "Edit Metadata", "Remove from Library" |
| 55 | +- Track removal with confirmation dialog |
| 56 | +- Shows selected track count in menu labels |
| 57 | + |
| 58 | +### 2. Now Playing View Components |
| 59 | + |
| 60 | +#### JavaScript Component (`app/frontend/js/components/now-playing-view.js`) |
| 61 | +- Uses `queueDragReorderMixin()` for drag-and-drop |
| 62 | +- Virtual scroll state management |
| 63 | +- Lyrics fetching and display |
| 64 | +- Key properties needed for context menu: |
| 65 | + - `contextMenu`: null or { x, y, track, items } |
| 66 | + - `showPlaylistSubmenu`: boolean |
| 67 | + - `submenuOnLeft`: boolean |
| 68 | + |
| 69 | +#### HTML Template (`app/frontend/views/now-playing.html`) |
| 70 | +Queue item rendering (lines 206-270): |
| 71 | +- Queue item div with classes: `queue-item-wrapper`, `queue-item` |
| 72 | +- Shows drag handle on left |
| 73 | +- Track title and artist in middle |
| 74 | +- Remove button (X) on right |
| 75 | +- Double-click handler: `@dblclick.stop="$store.queue.playIndex(item.originalIndex)"` |
| 76 | +- Current track highlighted with `item.isCurrentTrack ? 'bg-primary/20 text-primary' : ''` |
| 77 | + |
| 78 | +Currently NO right-click handler on queue items. |
| 79 | + |
| 80 | +### 3. Queue Store and API |
| 81 | + |
| 82 | +#### Queue Store (`app/frontend/js/stores/queue.js`) |
| 83 | +Key properties: |
| 84 | +- `items`: tracks in play order |
| 85 | +- `currentIndex`: currently playing track index |
| 86 | +- `playOrderItems`: computed getter that returns items with metadata for UI |
| 87 | + |
| 88 | +Key methods for context menu actions: |
| 89 | +- `async playNextTracks(tracks)`: Insert tracks after current track |
| 90 | + - Handles move semantics: removes track from queue before re-inserting at play-next position |
| 91 | + - Tracks play-next tracks in `_playNextTrackIds` Set for shuffle preservation |
| 92 | + - Uses `_playNextOffset` to append after previously queued-next tracks |
| 93 | + |
| 94 | +- `async remove(index)`: Remove track at index |
| 95 | + - Updates local state |
| 96 | + - Adjusts currentIndex if needed |
| 97 | + - Calls `queueApi.remove(index)` |
| 98 | + |
| 99 | +- `async playIndex(index, fromNavigation)`: Play track at index |
| 100 | + - Pushes current track to history (for prev button) |
| 101 | + - Resets `_playNextOffset` |
| 102 | + - Calls `player.playTrack(track)` and `queueApi.setCurrentIndex()` |
| 103 | + |
| 104 | +#### Queue API (`app/frontend/js/api/queue.js`) |
| 105 | +- `queue.add(trackIds, position)`: Add tracks at position |
| 106 | +- `queue.remove(position)`: Remove track at position |
| 107 | +- `queue.playNextTracks()`: Not exposed directly, handled by store |
| 108 | + |
| 109 | +### 4. Library Browser Implementation Reference |
| 110 | + |
| 111 | +#### Position Calculation Logic (`library-browser.js` lines 164-172) |
| 112 | +```javascript |
| 113 | +const menuHeight = 320; |
| 114 | +const menuWidth = 200; |
| 115 | +const submenuWidth = 200; |
| 116 | +let x = event.clientX; |
| 117 | +let y = event.clientY; |
| 118 | + |
| 119 | +if (x + menuWidth > window.innerWidth) { |
| 120 | + x = window.innerWidth - menuWidth - 10; |
| 121 | +} |
| 122 | +if (y + menuHeight > window.innerHeight) { |
| 123 | + y = window.innerHeight - menuHeight - 10; |
| 124 | +} |
| 125 | + |
| 126 | +this.submenuOnLeft = x + menuWidth + 45 + submenuWidth > window.innerWidth; |
| 127 | +``` |
| 128 | + |
| 129 | +#### Submenu Style Calculation (`library-browser.js` lines 642-647) |
| 130 | +```javascript |
| 131 | +getSubmenuStyle() { |
| 132 | + if (!this.contextMenu) return ''; |
| 133 | + const left = this.submenuOnLeft ? |
| 134 | + this.contextMenu.x - 180 : |
| 135 | + this.contextMenu.x + 180 + 45; |
| 136 | + const maxHeight = window.innerHeight - this.submenuY - 10; |
| 137 | + return `left: ${left}px; top: ${this.submenuY}px; max-height: ${maxHeight}px; overflow-y: auto`; |
| 138 | +} |
| 139 | +``` |
| 140 | + |
| 141 | +#### Menu Item Click Handler (`library-browser.js` lines 612-619) |
| 142 | +```javascript |
| 143 | +handleContextMenuItemClick(item) { |
| 144 | + if (!item.disabled && !item.hasSubmenu) { |
| 145 | + item.action(); |
| 146 | + this.contextMenu = null; |
| 147 | + } else if (!item.disabled) { |
| 148 | + item.action(); |
| 149 | + } |
| 150 | +} |
| 151 | +``` |
| 152 | + |
| 153 | +#### Submenu Mouse Handlers (`library-browser.js` lines 621-640) |
| 154 | +- `handleSubmenuMouseenter()`: Shows submenu, stores Y position for alignment |
| 155 | +- `handleSubmenuMouseleave()`: Sets timeout to hide submenu (200ms delay) |
| 156 | +- Timeout cleared on submenu enter to prevent flickering |
| 157 | + |
| 158 | +## Implementation Plan |
| 159 | + |
| 160 | +### For Now Playing View |
| 161 | + |
| 162 | +1. **Add context menu data properties** to `nowPlayingView` component |
| 163 | + - `contextMenu`: null | { x, y, track, items } |
| 164 | + - `playlists`: array |
| 165 | + - `showPlaylistSubmenu`: boolean |
| 166 | + - `submenuOnLeft`: boolean |
| 167 | + - `submenuY`: number |
| 168 | + - `submenuCloseTimeout`: null | timeout ID |
| 169 | + |
| 170 | +2. **Add right-click handler to queue items** in now-playing.html |
| 171 | + - Add `@contextmenu.prevent="handleContextMenu($event, item.track)"` to queue-item div |
| 172 | + |
| 173 | +3. **Add context menu methods to nowPlayingView**: |
| 174 | + - `handleContextMenu(event, track)`: Create menu with queue-specific items |
| 175 | + - `handleContextMenuItemClick(item)`: Click handler |
| 176 | + - `handleSubmenuMouseenter(item, el)`: Submenu hover enter |
| 177 | + - `handleSubmenuMouseleave(item)`: Submenu hover leave |
| 178 | + - `getSubmenuStyle()`: Calculate submenu position |
| 179 | + |
| 180 | +4. **Add context menu HTML** to now-playing.html after the queue list div |
| 181 | + - Main context menu div (similar to library.html) |
| 182 | + - Playlist submenu div |
| 183 | + - Reuse exact CSS classes for styling |
| 184 | + |
| 185 | +5. **Menu items for Now Playing context**: |
| 186 | + - Play Now (clear queue, add track, play) |
| 187 | + - Add to Queue (append to end) |
| 188 | + - separator |
| 189 | + - Play Next (insert after current) |
| 190 | + - Add to Playlist (with submenu) |
| 191 | + - Add to Liked Songs |
| 192 | + - separator |
| 193 | + - Show in Finder |
| 194 | + - separator |
| 195 | + - Remove from Queue (simple remove) |
| 196 | + |
| 197 | +6. **Action implementations**: |
| 198 | + - Use `this.queue` store methods: `playNextTracks()`, `remove()`, `playIndex()` |
| 199 | + - Use `favorites.add()`, `favorites.remove()`, `favorites.check()` |
| 200 | + - Use `playlists.addTracks()` |
| 201 | + - For "Show in Finder": use Tauri `show_in_folder()` command |
| 202 | + |
| 203 | +7. **Mixin approach** (optional, cleaner): |
| 204 | + - Create `now-playing-context-menu.js` mixin |
| 205 | + - Mix into nowPlayingView alongside `queueDragReorderMixin` |
| 206 | + - Keeps code modular and reusable |
| 207 | + |
| 208 | +## Key Differences from Library View |
| 209 | + |
| 210 | +1. **No multi-select**: Now Playing is single-track context menu (like artists/albums) |
| 211 | + - Should use `singleTrackContextMenuMixin` pattern |
| 212 | + - Simpler menu with fewer items |
| 213 | + |
| 214 | +2. **Queue-specific actions**: |
| 215 | + - "Play Next" uses `queue.playNextTracks()` not generic insert |
| 216 | + - "Remove from Queue" uses `queue.remove()` not playlist-specific removal |
| 217 | + - No "Edit Metadata" (queue tracks are read-only references) |
| 218 | + - No "Remove from Library" (would remove from library, not queue) |
| 219 | + |
| 220 | +3. **Position context**: |
| 221 | + - Queue list is inside fixed right panel (w-96) |
| 222 | + - Menu positioning needs to account for panel boundaries |
| 223 | + - Container rect calculations needed for accurate positioning |
| 224 | + |
| 225 | +4. **No drag conflicts**: |
| 226 | + - Context menu right-click shouldn't interfere with drag-handle left-side |
| 227 | + - Drag handle is on left (0.5rem gap), context menu typically triggered on track content |
| 228 | + |
| 229 | +## Testing Considerations |
| 230 | + |
| 231 | +- AC#1: Right-click on queue track opens menu |
| 232 | +- AC#2: Menu items present and functional |
| 233 | +- AC#3: Menu flips left when near right edge of screen |
| 234 | +- AC#4: Play Next reorders queue visually updates |
| 235 | +- AC#5: Click outside or select item dismisses menu |
| 236 | +- AC#6: Drag-to-reorder still works, remove button still works |
| 237 | + |
| 238 | +## Files to Modify |
| 239 | + |
| 240 | +1. `app/frontend/js/components/now-playing-view.js` - Add properties and methods |
| 241 | +2. `app/frontend/views/now-playing.html` - Add @contextmenu handler and context menu HTML |
| 242 | +3. Create or reuse `app/frontend/js/mixins/now-playing-context-menu.js` - Optional mixin file |
| 243 | + |
| 244 | +## CSS Already Available |
| 245 | + |
| 246 | +All context menu styles already exist in `app/frontend/styles.css` lines 579-630. |
| 247 | +No new CSS needed. |
0 commit comments