From e13151ffa5dc86e41ce242421679d65a740c3af0 Mon Sep 17 00:00:00 2001 From: Caleb Allen Date: Sat, 28 Feb 2026 14:14:29 -0500 Subject: [PATCH] feat(quick-3): add playlist-level multi-select state and selection handling - Add selectedPlaylists Set and lastSelectedPlaylistIndex for multi-select state - Replace handleToggle with handlePlaylistHeaderClick supporting Ctrl/Shift+Click - Clear playlist selection when entering track selection scope - Clear playlist selection on outside clicks - Add .playlist-header.selected CSS with selection background color - Wire header click to new handler with selected class in template --- .../components/playlist-view/playlist-view.ts | 80 ++++++++++++++++++- 1 file changed, 76 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/playlist-view/playlist-view.ts b/frontend/src/components/playlist-view/playlist-view.ts index 8025c07..8ea7fdc 100644 --- a/frontend/src/components/playlist-view/playlist-view.ts +++ b/frontend/src/components/playlist-view/playlist-view.ts @@ -187,6 +187,12 @@ export class PlaylistView @state() private renamingPlaylistIndex = -1; @state() private renameValue = ''; + /** Indices of playlists selected via Ctrl/Shift+Click. */ + @state() private selectedPlaylists: Set = new Set(); + + /** Anchor index for Shift+Click range selection on playlists. */ + private lastSelectedPlaylistIndex: number | null = null; + /** Index of the playlist currently hovered during a drag. */ @state() private dragOverPlaylistIndex = -1; @@ -248,10 +254,21 @@ export class PlaylistView el.classList.contains('track-item') && this.shadowRoot?.contains(el), ); + const isPlaylistHeaderClick = path.some( + (el) => + el instanceof HTMLElement && + el.classList.contains('playlist-header') && + this.shadowRoot?.contains(el), + ); if (!isTrackClick) { this.selection.clear(); } + + if (!isPlaylistHeaderClick && !isTrackClick) { + this.selectedPlaylists = new Set(); + this.lastSelectedPlaylistIndex = null; + } }; // ================================================================= @@ -329,6 +346,12 @@ export class PlaylistView private ensureSelectionScope( playlistIndex: number, ): void { + // Clear playlist-level selection when entering track selection + if (this.selectedPlaylists.size > 0) { + this.selectedPlaylists = new Set(); + this.lastSelectedPlaylistIndex = null; + } + if ( this.activePlaylistIndex !== playlistIndex ) { @@ -496,6 +519,10 @@ export class PlaylistView background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05)); } + .playlist-header.selected { + background-color: var(--yj-selection-bg, rgba(100, 160, 255, 0.15)); + } + .playlist-item.drag-over > .playlist-header { background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15)); outline: 1px dashed var(--yj-accent, #ffd43b); @@ -1022,11 +1049,56 @@ export class PlaylistView } } - private handleToggle = (index: number) => { + private handlePlaylistHeaderClick = ( + e: MouseEvent, + index: number, + ) => { const entry = this.entries[index]; if (!entry) return; + const isCtrl = e.ctrlKey || e.metaKey; + const isShift = e.shiftKey; + + if (isCtrl) { + // Ctrl/Cmd+Click: toggle playlist in selection + const next = new Set(this.selectedPlaylists); + + if (next.has(index)) { + next.delete(index); + } else { + next.add(index); + } + + this.selectedPlaylists = next; + this.lastSelectedPlaylistIndex = index; + // Clear track-level selection + this.selection.clear(); + this.activePlaylistIndex = -1; + return; + } + + if (isShift && this.lastSelectedPlaylistIndex !== null) { + // Shift+Click: range-select playlists + const start = Math.min(this.lastSelectedPlaylistIndex, index); + const end = Math.max(this.lastSelectedPlaylistIndex, index); + const next = new Set(this.selectedPlaylists); + + for (let i = start; i <= end; i++) { + next.add(i); + } + + this.selectedPlaylists = next; + // Clear track-level selection + this.selection.clear(); + this.activePlaylistIndex = -1; + return; + } + + // Plain click: clear playlist selection, toggle expand/collapse + this.selectedPlaylists = new Set(); + this.lastSelectedPlaylistIndex = null; + // If collapsing the active playlist, clear selection. if ( entry.expanded && @@ -2409,9 +2481,9 @@ export class PlaylistView this.onPlaylistDrop(e, index)} >
- this.handleToggle(index)} + class="playlist-header ${this.selectedPlaylists.has(index) ? 'selected' : ''}" + @click=${(e: MouseEvent) => + this.handlePlaylistHeaderClick(e, index)} @contextmenu=${(e: MouseEvent) => this.handlePlaylistContextMenu( e,