From d89dee44e3eb4501d3dceab09b4b493e40b957e2 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Mon, 23 Feb 2026 09:42:11 -0500 Subject: [PATCH] single track drag-and-drop ghost --- .../src/components/cover-grid/cover-grid.ts | 14 +++- .../components/playlist-view/playlist-view.ts | 12 ++- .../src/components/queue-panel/queue-panel.ts | 19 ++++- .../src/components/track-list/track-list.ts | 12 ++- frontend/src/utils/drag-image.ts | 77 +++++++++++++++++++ 5 files changed, 122 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index ce0e0c1..a694e03 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -39,6 +39,7 @@ import type { DragPayload } from '@utils/drag-controller'; import { createAlbumArtDragImage, createDragImage, + createTrackCardDragImage, removeDragImage, } from '@utils/drag-image'; @@ -2850,9 +2851,16 @@ export class CoverGrid extends LitElement { JSON.stringify(payload), ); - this.dragImageEl = createDragImage( - filePaths.length, - ); + this.dragImageEl = + filePaths.length === 1 + ? createTrackCardDragImage( + track.TrackName, + track.ArtistName, + track.FilePath, + ) + : createDragImage( + filePaths.length, + ); dataTransfer.setDragImage( this.dragImageEl, 0, diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts index 4dd44ff..66b29a5 100644 --- a/frontend/src/components/playlist-view/playlist-view.ts +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -37,6 +37,7 @@ import { } from '@utils/drag-controller'; import { createDragImage, + createTrackCardDragImage, removeDragImage, } from '@utils/drag-image'; @@ -1143,9 +1144,14 @@ export class PlaylistView sourcePlaylistId: entry.summary.ID, }); - this.dragImageEl = createDragImage( - filePaths.length, - ); + this.dragImageEl = + filePaths.length === 1 + ? createTrackCardDragImage( + track.Title, + track.Artist, + track.FilePath, + ) + : createDragImage(filePaths.length); e.dataTransfer?.setDragImage( this.dragImageEl, 0, diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index e6a035d..d8900eb 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -26,6 +26,7 @@ import { } from '@utils/drag-controller'; import { createDragImage, + createTrackCardDragImage, removeDragImage, } from '@utils/drag-image'; @@ -1096,9 +1097,21 @@ export class QueuePanel source: 'queue', }); - this.dragImageEl = createDragImage( - filePaths.length, - ); + if (filePaths.length === 1) { + const t = tracks[index]!; + + this.dragImageEl = + createTrackCardDragImage( + t.title, + t.artist, + t.filePath, + ); + } else { + this.dragImageEl = createDragImage( + filePaths.length, + ); + } + e.dataTransfer?.setDragImage( this.dragImageEl, 0, diff --git a/frontend/src/components/track-list/track-list.ts b/frontend/src/components/track-list/track-list.ts index 217e665..b07d5e0 100644 --- a/frontend/src/components/track-list/track-list.ts +++ b/frontend/src/components/track-list/track-list.ts @@ -21,6 +21,7 @@ import { } from '@utils/drag-controller'; import { createDragImage, + createTrackCardDragImage, removeDragImage, } from '@utils/drag-image'; import '@lit-labs/virtualizer'; @@ -1135,9 +1136,14 @@ export class TrackList extends LitElement implements SelectionHost { }); // Custom drag image. - this.dragImageEl = createDragImage( - filePaths.length, - ); + this.dragImageEl = + filePaths.length === 1 + ? createTrackCardDragImage( + track.TrackName, + track.ArtistName, + track.FilePath, + ) + : createDragImage(filePaths.length); e.dataTransfer?.setDragImage( this.dragImageEl, 0, diff --git a/frontend/src/utils/drag-image.ts b/frontend/src/utils/drag-image.ts index 5df6b5a..d0dff31 100644 --- a/frontend/src/utils/drag-image.ts +++ b/frontend/src/utils/drag-image.ts @@ -66,6 +66,83 @@ export function createAlbumArtDragImage( return wrapper; } +/** + * Creates a drag image styled like a queue track card showing the + * track title and artist. Used when dragging a single track. + * + * If `title` is empty and `filePath` is provided the filename + * (without extension) is used as a fallback. An empty `artist` + * falls back to "Unknown Artist". + */ +export function createTrackCardDragImage( + title: string, + artist: string, + filePath?: string, +): HTMLElement { + let displayTitle = title; + + if (!displayTitle && filePath) { + const parts = filePath.split(/[\\/]/); + const filename = + parts[parts.length - 1] ?? filePath; + + displayTitle = filename.replace(/\.[^.]+$/, ''); + } + + if (!displayTitle) { + displayTitle = 'Unknown Title'; + } + + const displayArtist = artist || 'Unknown Artist'; + + const card = document.createElement('div'); + + card.style.cssText = [ + 'position: fixed', + 'top: -1000px', + 'left: -1000px', + 'max-width: 220px', + 'padding: 8px 14px', + 'border-radius: 6px', + 'background: #2a2a2a', + 'box-shadow: 0 2px 8px rgba(0,0,0,0.4)', + 'pointer-events: none', + 'z-index: 9999', + 'display: flex', + 'flex-direction: column', + 'gap: 2px', + 'font-family: inherit', + ].join(';'); + + const titleEl = document.createElement('span'); + + titleEl.textContent = displayTitle; + titleEl.style.cssText = [ + 'font-size: 13px', + 'color: #fff', + 'white-space: nowrap', + 'overflow: hidden', + 'text-overflow: ellipsis', + ].join(';'); + + const artistEl = document.createElement('span'); + + artistEl.textContent = displayArtist; + artistEl.style.cssText = [ + 'font-size: 11px', + 'color: #b3b3b3', + 'white-space: nowrap', + 'overflow: hidden', + 'text-overflow: ellipsis', + ].join(';'); + + card.appendChild(titleEl); + card.appendChild(artistEl); + document.body.appendChild(card); + + return card; +} + /** Remove a drag image element created by createDragImage. */ export function removeDragImage(el: HTMLElement): void { el.remove();