3.6 KiB
| title | date | status |
|---|---|---|
| Track Context Menu on Bottom Controls | 2026-05-30 | 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:
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
lastUsedPlaylistNameis set andonAddToLastPlaylistis non-nil. - "Add to Playlist" submenu — one
Buttonper playlist inplaylists. CallsonAddToPlaylist(track, playlist). - Divider + "Remove from Playlist" — shown only when
selectedPlaylist != nilandonRemoveFromPlaylistis non-nil.
Menu is omitted entirely (no empty menu flicker) when track or config is nil.
PlayerControlsView Changes
PlayerControlsView gains one new optional parameter:
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
NSMenupath inTrackTableViewis kept — SwiftUI.contextMenudoes not attach per-row in anNSTableView, so the table continues usingmenuNeedsUpdate.
ContentView Changes
ContentView constructs one TrackContextMenuConfig and passes it to both views:
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