--- title: Track Context Menu on Bottom Controls date: 2026-05-30 status: approved --- ## Goal Right-clicking the now-playing area (bottom-left of the window) shows the same context menu as right-clicking a track in the track table: add to last playlist, add to any playlist via submenu, and remove from the current playlist when one is open. ## Shared Config Struct A new `TrackContextMenuConfig` struct captures everything the menu needs: ```swift struct TrackContextMenuConfig { let playlists: [Playlist] let lastUsedPlaylistName: String? let selectedPlaylist: Playlist? let onAddToPlaylist: (Track, Playlist) -> Void let onAddToLastPlaylist: ((Track) -> Void)? let onRemoveFromPlaylist: ((Track) -> Void)? } ``` This is the single source of truth for menu data. Both `TrackTableView` and `PlayerControlsView` receive one instance, constructed by `ContentView`. ## Shared ViewModifier `TrackContextMenuModifier` is a SwiftUI `ViewModifier` that takes a `Track?` and `TrackContextMenuConfig?` and applies `.contextMenu` when both are non-nil: - **"Add to [last]"** button — shown only when `lastUsedPlaylistName` is set and `onAddToLastPlaylist` is non-nil. - **"Add to Playlist"** submenu — one `Button` per playlist in `playlists`. Calls `onAddToPlaylist(track, playlist)`. - **Divider + "Remove from Playlist"** — shown only when `selectedPlaylist != nil` and `onRemoveFromPlaylist` is non-nil. Menu is omitted entirely (no empty menu flicker) when `track` or `config` is nil. ## PlayerControlsView Changes `PlayerControlsView` gains one new optional parameter: ```swift let contextMenuConfig: TrackContextMenuConfig? ``` The `nowPlayingSection` view applies `.trackContextMenu(track: currentTrack, config: contextMenuConfig)` (a convenience extension wrapping `TrackContextMenuModifier`). ## TrackTableView Refactor `TrackTableView`'s existing four playlist-related parameters (`playlists`, `lastUsedPlaylistName`, `selectedPlaylist`, `onAddToLastPlaylist`, `onRemoveFromPlaylist`) are replaced by a single `contextMenuConfig: TrackContextMenuConfig?`. The `Coordinator.menuNeedsUpdate` builds its `NSMenu` from this config. This makes both call sites symmetric. > The AppKit `NSMenu` path in `TrackTableView` is kept — SwiftUI `.contextMenu` does not attach per-row in an `NSTableView`, so the table continues using `menuNeedsUpdate`. ## ContentView Changes `ContentView` constructs one `TrackContextMenuConfig` and passes it to both views: ```swift let menuConfig = TrackContextMenuConfig( playlists: playlist.allPlaylists, lastUsedPlaylistName: playlist.lastUsedPlaylistName, selectedPlaylist: playlist.selectedPlaylist, onAddToPlaylist: { track, pl in try? playlist.addTrack(track, to: pl) }, onAddToLastPlaylist: { track in try? playlist.addTrackToLastUsedPlaylist(track) }, onRemoveFromPlaylist: playlist.selectedPlaylist != nil ? { track in if let selected = playlist.selectedPlaylist { try? playlist.removeTrack(track, from: selected) } } : nil ) ``` ## Files Affected | File | Change | |------|--------| | `Music/Models/TrackContextMenuConfig.swift` | New file — struct definition | | `Music/Views/TrackContextMenuModifier.swift` | New file — SwiftUI ViewModifier | | `Music/Views/PlayerControlsView.swift` | Add `contextMenuConfig` param, apply modifier to `nowPlayingSection` | | `Music/Views/TrackTableView.swift` | Replace individual playlist params with `contextMenuConfig`, adapt `menuNeedsUpdate` | | `Music/ContentView.swift` | Construct and pass `TrackContextMenuConfig` to both views | ## Out of Scope - Keyboard shortcut for the menu - Any new menu items not already in the track table menu