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.
85 lines
3.5 KiB
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
|
|
|