You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
Music/docs/superpowers/specs/2026-05-30-add-to-new-playl...

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 playlists is 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 a Divider before the list of existing playlists.
  • Relax the visibility guard so the submenu shows when !config.playlists.isEmpty || config.onAddToNewPlaylist != nil (previously hidden whenever playlists was empty).

4. ContentView (Music/ContentView.swift)

  • Add state: @State private var newPlaylistTrack: Track? and a name field (reuse/parallel the existing playlistNameInput style; a dedicated field is fine).
  • In trackContextMenuConfig, wire onAddToNewPlaylist: { track in newPlaylistTrack = track }.
  • Present an alert (mirroring the existing New Playlist alert) gated on newPlaylistTrack != nil. Create trims whitespace, and if non-empty calls playlist.createPlaylistAndAddTrack(name:track:) with the pending track; Cancel and completion both clear newPlaylistTrack and the name field.

Testing (TDD)

Unit-test PlaylistViewModel.createPlaylistAndAddTrack(name:track:) against an in-memory database:

  1. Seed a track in the DB.
  2. Call createPlaylistAndAddTrack(name:track:).
  3. Assert a regular playlist with that name now exists.
  4. Assert the playlist's tracks contain the seeded track.
  5. Assert lastUsedPlaylistId equals 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.