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...

110 lines
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:
```swift
@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 <last>" 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):
```swift
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.