diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index 540f4ad..21038a5 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -1480,12 +1480,16 @@ export class CoverGrid source: 'cover-grid', }); - // Single album: show cover art thumbnail. - // Multiple albums: show track-count badge. + // Single album: show its cover, badged with how many tracks are + // on the way -- an album is 1 track or 30 and the thumbnail is + // the same picture either way, so the number the drop is about + // was the one thing this drag did not say. + // Multiple albums: show the track-count badge alone. if (isSingleAlbum && hit.album.CoverArtPath) { this.dragImageEl = createAlbumArtDragImage( this.getCoverUrl(hit.album), + filePaths.length, ); } else { this.dragImageEl = createDragImage( diff --git a/frontend/src/utils/drag-image.ts b/frontend/src/utils/drag-image.ts index d0dff31..6002dcb 100644 --- a/frontend/src/utils/drag-image.ts +++ b/frontend/src/utils/drag-image.ts @@ -29,11 +29,23 @@ export function createDragImage(count: number): HTMLElement { } /** - * Creates a drag image showing an album cover art thumbnail. - * Falls back to the track-count badge if the image fails to load. + * Creates a drag image showing an album cover art thumbnail, with a + * corner badge saying how many tracks are on the way. + * + * The count is not decoration. The cover says *what* is being dragged + * and nothing said *how much* — an album is 1 track or 30 and the + * thumbnail is identical either way, so the one number the drop is + * about was the one thing the drag did not show. Every other drag in + * the app says it (`createDragImage` is a count and nothing else); + * this one was the exception because it had a picture to show instead. + * + * A count of 1 draws no badge: "1" over a single album cover is noise, + * and the absence is unambiguous next to a badge that only ever + * appears when there is more than one. */ export function createAlbumArtDragImage( coverUrl: string, + count = 1, ): HTMLElement { const size = 64; const wrapper = document.createElement('div'); @@ -44,6 +56,12 @@ export function createAlbumArtDragImage( 'left: -1000px', 'pointer-events: none', 'z-index: 9999', + // The badge is positioned against this box, and the box stays + // exactly the cover's size: anything outside it risks being + // clipped out of the snapshot the browser takes, and padding + // it instead would move the cover away from the cursor. + `width: ${size}px`, + `height: ${size}px`, ].join(';'); const img = document.createElement('img'); @@ -61,11 +79,44 @@ export function createAlbumArtDragImage( ].join(';'); wrapper.appendChild(img); + + if (count > 1) { + wrapper.appendChild(countBadge(count)); + } + document.body.appendChild(wrapper); return wrapper; } +/** The corner badge on a multi-track drag image. */ +function countBadge(count: number): HTMLElement { + const badge = document.createElement('span'); + + badge.className = 'drag-count-badge'; + badge.textContent = String(count); + badge.style.cssText = [ + 'position: absolute', + 'top: 3px', + 'right: 3px', + 'min-width: 20px', + 'height: 20px', + 'padding: 0 5px', + 'box-sizing: border-box', + 'border-radius: 10px', + 'background: #ffd43b', + 'color: #000', + 'font-size: 12px', + 'font-weight: 600', + 'font-family: inherit', + 'line-height: 20px', + 'text-align: center', + 'box-shadow: 0 1px 4px rgba(0,0,0,0.5)', + ].join(';'); + + return badge; +} + /** * Creates a drag image styled like a queue track card showing the * track title and artist. Used when dragging a single track. diff --git a/frontend/test/components/drag-image.test.ts b/frontend/test/components/drag-image.test.ts new file mode 100644 index 0000000..c436e5c --- /dev/null +++ b/frontend/test/components/drag-image.test.ts @@ -0,0 +1,71 @@ +/** + * A drag says how much it is carrying. + * + * Every drag in the app already did — `createDragImage` is a count and + * nothing else — except the one with a picture to show instead. An + * album dragged to the queue put its cover under the cursor and said + * nothing about how many tracks that was, and an album is 1 track or 30 + * with the same thumbnail either way. The number is the thing the drop + * is about. + * + * A count of 1 draws no badge: "1" over a single cover is noise, and + * the absence reads unambiguously beside a badge that only ever appears + * when there is more than one. + */ +import { describe, expect, it, afterEach } from 'vitest'; + +import { + createAlbumArtDragImage, + removeDragImage, +} from '@utils/drag-image'; + +const made: HTMLElement[] = []; + +function dragImage(count?: number): HTMLElement { + const el = + count === undefined + ? createAlbumArtDragImage('data:image/gif;base64,R0lGODlhAQABAAAAACw=') + : createAlbumArtDragImage( + 'data:image/gif;base64,R0lGODlhAQABAAAAACw=', + count, + ); + + made.push(el); + + return el; +} + +const badge = (el: HTMLElement) => + el.querySelector('.drag-count-badge'); + +describe('the album drag image', () => { + afterEach(() => { + while (made.length > 0) removeDragImage(made.pop()!); + }); + + it('says how many tracks are being dragged', () => { + expect(badge(dragImage(12))?.textContent).toBe('12'); + }); + + it('says nothing when there is only one track', () => { + expect(badge(dragImage(1))).toBeNull(); + }); + + it('still draws a bare cover for a caller that gives no count', () => { + // The count is optional so the helper stays usable from a call site + // that has a cover and no list; it must not badge such a drag "1". + expect(badge(dragImage())).toBeNull(); + }); + + it('keeps the badge inside the cover', () => { + // setDragImage snapshots the element, and anything outside its box + // risks being clipped out of that snapshot — while padding the box + // instead would move the cover away from the cursor. + const el = dragImage(30); + const outer = el.getBoundingClientRect(); + const mark = badge(el)!.getBoundingClientRect(); + + expect(mark.right).toBeLessThanOrEqual(outer.right); + expect(mark.top).toBeGreaterThanOrEqual(outer.top); + }); +});