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