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
This commit is contained in:
@@ -187,6 +187,12 @@ export class PlaylistView
|
|||||||
@state() private renamingPlaylistIndex = -1;
|
@state() private renamingPlaylistIndex = -1;
|
||||||
@state() private renameValue = '';
|
@state() private renameValue = '';
|
||||||
|
|
||||||
|
/** Indices of playlists selected via Ctrl/Shift+Click. */
|
||||||
|
@state() private selectedPlaylists: Set<number> = 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. */
|
/** Index of the playlist currently hovered during a drag. */
|
||||||
@state() private dragOverPlaylistIndex = -1;
|
@state() private dragOverPlaylistIndex = -1;
|
||||||
|
|
||||||
@@ -248,10 +254,21 @@ export class PlaylistView
|
|||||||
el.classList.contains('track-item') &&
|
el.classList.contains('track-item') &&
|
||||||
this.shadowRoot?.contains(el),
|
this.shadowRoot?.contains(el),
|
||||||
);
|
);
|
||||||
|
const isPlaylistHeaderClick = path.some(
|
||||||
|
(el) =>
|
||||||
|
el instanceof HTMLElement &&
|
||||||
|
el.classList.contains('playlist-header') &&
|
||||||
|
this.shadowRoot?.contains(el),
|
||||||
|
);
|
||||||
|
|
||||||
if (!isTrackClick) {
|
if (!isTrackClick) {
|
||||||
this.selection.clear();
|
this.selection.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isPlaylistHeaderClick && !isTrackClick) {
|
||||||
|
this.selectedPlaylists = new Set();
|
||||||
|
this.lastSelectedPlaylistIndex = null;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// =================================================================
|
// =================================================================
|
||||||
@@ -329,6 +346,12 @@ export class PlaylistView
|
|||||||
private ensureSelectionScope(
|
private ensureSelectionScope(
|
||||||
playlistIndex: number,
|
playlistIndex: number,
|
||||||
): void {
|
): void {
|
||||||
|
// Clear playlist-level selection when entering track selection
|
||||||
|
if (this.selectedPlaylists.size > 0) {
|
||||||
|
this.selectedPlaylists = new Set();
|
||||||
|
this.lastSelectedPlaylistIndex = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.activePlaylistIndex !== playlistIndex
|
this.activePlaylistIndex !== playlistIndex
|
||||||
) {
|
) {
|
||||||
@@ -496,6 +519,10 @@ export class PlaylistView
|
|||||||
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.05));
|
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 {
|
.playlist-item.drag-over > .playlist-header {
|
||||||
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
background-color: var(--yj-accent-bg-strong, rgba(255, 212, 59, 0.15));
|
||||||
outline: 1px dashed var(--yj-accent, #ffd43b);
|
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];
|
const entry = this.entries[index];
|
||||||
|
|
||||||
if (!entry) return;
|
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 collapsing the active playlist, clear selection.
|
||||||
if (
|
if (
|
||||||
entry.expanded &&
|
entry.expanded &&
|
||||||
@@ -2409,9 +2481,9 @@ export class PlaylistView
|
|||||||
this.onPlaylistDrop(e, index)}
|
this.onPlaylistDrop(e, index)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="playlist-header"
|
class="playlist-header ${this.selectedPlaylists.has(index) ? 'selected' : ''}"
|
||||||
@click=${() =>
|
@click=${(e: MouseEvent) =>
|
||||||
this.handleToggle(index)}
|
this.handlePlaylistHeaderClick(e, index)}
|
||||||
@contextmenu=${(e: MouseEvent) =>
|
@contextmenu=${(e: MouseEvent) =>
|
||||||
this.handlePlaylistContextMenu(
|
this.handlePlaylistContextMenu(
|
||||||
e,
|
e,
|
||||||
|
|||||||
Reference in New Issue
Block a user