4.6 KiB
Add "New Playlist…" to the Add-to-Playlist menu
Date: 2026-05-30 Status: Approved design
Goal
In a track's right-click "Add to Playlist" submenu, let the user create a brand-new regular playlist on the fly: pick New Playlist…, enter a name, and on save the playlist is created and the track is added to it.
Background
The "Add to Playlist" submenu lives in TrackContextMenuModifier.swift and is driven by
the data-only TrackContextMenuConfig struct (a playlists array plus action closures),
built in ContentView.trackContextMenuConfig (ContentView.swift:415).
The app already creates playlists from the sidebar via an .alert + TextField
(ContentView.swift:273), calling PlaylistViewModel.createPlaylist(name:) →
DatabaseService.createPlaylist(name:) -> Playlist. Adding a track is
PlaylistViewModel.addTrack(_:to:), which also records the playlist as last-used.
Regular and smart playlists are separate tables; this feature only creates regular
playlists (Playlist).
Approach
Route the name prompt up to ContentView, which already owns the new-playlist alert.
SwiftUI alerts do not present reliably when attached inside a context menu's content
(the menu dismisses, and the per-row modifier is re-instantiated). So the menu item only
signals intent — "create a new playlist for this track" — and ContentView owns the
prompt and the orchestration. This reuses the existing alert pattern rather than
duplicating it inside the modifier (the rejected alternative, which would also need the
modifier to reach into PlaylistViewModel).
Behavior decisions
- Adds only the clicked track (matches the current single-track "Add to Playlist").
- No navigation — after create+add the sidebar selection is unchanged.
- Empty / whitespace-only name → no-op (matches the existing create flow).
- When the user has no existing playlists, the submenu still appears showing just
"New Playlist…" (today the whole submenu is hidden when
playlistsis empty). - Remote mode → wired the same way as the existing "Add to Playlist" action (unconditionally), keeping parity with current behavior.
Changes
1. PlaylistViewModel (Music/ViewModels/PlaylistViewModel.swift)
Add one orchestration method — the unit under test:
@discardableResult
func createPlaylistAndAddTrack(name: String, track: Track) throws -> Playlist {
let playlist = try db.createPlaylist(name: name)
try addTrack(track, to: playlist)
return playlist
}
db.createPlaylist returns a Playlist with its assigned id; addTrack adds the track
and sets lastUsedPlaylistId to the new playlist (so the "Add to " item updates too).
2. TrackContextMenuConfig (Music/Models/TrackContextMenuConfig.swift)
Add a new optional closure, defaulting to nil (so all existing call sites and tests
compile unchanged):
let onAddToNewPlaylist: ((Track) -> Void)?
Add it to the explicit init with a = nil default, alongside the other optionals.
3. TrackContextMenuModifier (Music/Views/TrackContextMenuModifier.swift)
Inside the "Add to Playlist" submenu:
- Add a New Playlist… button at the top (ellipsis = opens a prompt) when
onAddToNewPlaylist != nil, followed by aDividerbefore the list of existing playlists. - Relax the visibility guard so the submenu shows when
!config.playlists.isEmpty || config.onAddToNewPlaylist != nil(previously hidden wheneverplaylistswas empty).
4. ContentView (Music/ContentView.swift)
- Add state:
@State private var newPlaylistTrack: Track?and a name field (reuse/parallel the existingplaylistNameInputstyle; a dedicated field is fine). - In
trackContextMenuConfig, wireonAddToNewPlaylist: { track in newPlaylistTrack = track }. - Present an alert (mirroring the existing New Playlist alert) gated on
newPlaylistTrack != nil. Create trims whitespace, and if non-empty callsplaylist.createPlaylistAndAddTrack(name:track:)with the pending track; Cancel and completion both clearnewPlaylistTrackand the name field.
Testing (TDD)
Unit-test PlaylistViewModel.createPlaylistAndAddTrack(name:track:) against an in-memory
database:
- Seed a track in the DB.
- Call
createPlaylistAndAddTrack(name:track:). - Assert a regular playlist with that name now exists.
- Assert the playlist's tracks contain the seeded track.
- Assert
lastUsedPlaylistIdequals the new playlist's id.
The SwiftUI menu/alert rendering is not unit-tested, consistent with the rest of the
codebase. TrackContextMenuConfig's new optional defaults to nil, so existing
TrackContextMenuConfigTests are unaffected.