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-31-track-drag-to-pl...

85 lines
3.5 KiB

# Track Drag-to-Playlist Design
## Goal
Drag a single track from the track table onto a playlist chip in the bottom bar to add that track to the playlist.
## Scope
- Single track only (no multi-select drag)
- Regular playlists only (smart playlists are read-only)
- Duplicate adds silently ignored (DB `UNIQUE(playlistId, trackId)` constraint)
## Implementation
### 1. Custom Pasteboard Type
Define a custom `NSPasteboard.PasteboardType` for track IDs to avoid collisions with the existing `.string` type used for row-reorder and the `.fileURL` type used for Finder file drops.
Location: top of `TrackTableView.swift` (alongside existing private constants).
```swift
private let trackIdPasteboardType = NSPasteboard.PasteboardType("com.music.trackID")
```
### 2. Drag Source — TrackTableView Coordinator
**File:** `TrackTableView.swift`
**Change `pasteboardWriterForRow`:** Currently only writes when `onReorder != nil`. Change to always write the track ID under the custom type. When `onReorder` is also set, additionally write the row index under `.string` (preserving existing reorder behavior).
```
func pasteboardWriterForRow(row) -> NSPasteboardWriting?
let item = NSPasteboardItem()
// Always write track ID for cross-view drag
if let trackId = parent.tracks[row].id {
item.setString(String(trackId), forType: trackIdPasteboardType)
}
// Also write row index if reorder is enabled
if parent.onReorder != nil {
item.setString(String(row), forType: .string)
}
return item
```
**Register for drag types:** Add `trackIdPasteboardType` to `registerForDraggedTypes` alongside `.string`.
**`validateDrop` / `acceptDrop`:** Update to read from `.string` type specifically (not just first pasteboard item string), so they continue to work for reorder and ignore the track-ID type.
### 3. Drop Target — PlaylistBarView
**File:** `PlaylistBarView.swift`
**New callback prop:**
```swift
var onDropTrack: ((Int64, Playlist) -> Void)?
```
**Drop modifier on each regular playlist chip:** Add `.onDrop(of:isTargeted:perform:)` to each `PlaylistButton` for regular playlists only. The `isTargeted` binding drives a visual highlight (accent-colored border/background).
Since SwiftUI's `.onDrop` works with UTTypes, we register the custom type as a UTType and read the track ID from the `NSItemProvider`.
**Visual feedback:** When `isTargeted` is true, show a highlighted border/background on the chip (e.g. `tintColor.opacity(0.3)` background + `tintColor` border at 2pt).
**PlaylistButton changes:** Add an `isDropTarget: Bool` parameter to `PlaylistButton` that controls the highlight styling. Default `false`.
### 4. Wiring — ContentView
**File:** `ContentView.swift`
Pass the `onDropTrack` closure to `PlaylistBarView`. The closure calls `PlaylistViewModel.addTrack(_:to:)`, wrapping in try/catch to silently handle duplicates.
## Files Changed
| File | Change |
|------|--------|
| `TrackTableView.swift` | Custom pasteboard type constant; `pasteboardWriterForRow` writes track ID always + row index when reordering; register custom drag type; update `validateDrop`/`acceptDrop` to read `.string` explicitly |
| `PlaylistBarView.swift` | `onDropTrack` callback; `.onDrop` modifier on regular playlist chips; `isDropTarget` highlight state on `PlaylistButton` |
| `ContentView.swift` | Wire `onDropTrack` closure through to `PlaylistBarView` |
## Behaviors Preserved
- Right-click "Add to Playlist" context menu unchanged
- Drag reorder within a playlist unchanged
- File drops from Finder unchanged
- Smart playlists don't accept drops