cleaned up album selection behavior, context menu now closes when clicking outside of it

This commit is contained in:
2026-02-21 11:02:54 -05:00
parent 402789e763
commit 4d9ab5d2b8
5 changed files with 183 additions and 148 deletions
@@ -64,6 +64,7 @@ export class AlbumDropdown extends LitElement {
static override styles = css` static override styles = css`
:host { :host {
display: block; display: block;
margin-top: 14px;
} }
.album-dropdown { .album-dropdown {
@@ -76,12 +77,12 @@ export class AlbumDropdown extends LitElement {
.carat { .carat {
position: absolute; position: absolute;
top: -8px; top: -10px;
width: 0; width: 0;
height: 0; height: 0;
border-left: 9px solid transparent; border-left: 11px solid transparent;
border-right: 9px solid transparent; border-right: 11px solid transparent;
border-bottom: 8px solid var(--yj-bg-elevated, #343a40); border-bottom: 10px solid var(--yj-bg-elevated, #343a40);
} }
.dropdown-tracks { .dropdown-tracks {
@@ -343,21 +344,21 @@ export class AlbumDropdown extends LitElement {
class=${classes} class=${classes}
draggable="true" draggable="true"
@click=${(e: MouseEvent) => @click=${(e: MouseEvent) =>
this.onTrackClick(e, track, index)} this.onTrackClick(e, track, index)}
@dblclick=${(e: MouseEvent) => @dblclick=${(e: MouseEvent) =>
this.onTrackDblClick( this.onTrackDblClick(
e, e,
track, track,
index, index,
)} )}
@contextmenu=${(e: MouseEvent) => @contextmenu=${(e: MouseEvent) =>
this.onTrackContextMenu(e, track)} this.onTrackContextMenu(e, track)}
@dragstart=${(e: DragEvent) => @dragstart=${(e: DragEvent) =>
this.onTrackDragStart( this.onTrackDragStart(
e, e,
track, track,
index, index,
)} )}
@dragend=${() => this.onTrackDragEnd()} @dragend=${() => this.onTrackDragEnd()}
> >
<span class="track-number"> <span class="track-number">
@@ -371,8 +372,8 @@ export class AlbumDropdown extends LitElement {
</span> </span>
<span class="track-duration"> <span class="track-duration">
${formatMilliseconds( ${formatMilliseconds(
track.TrackLength, track.TrackLength,
)} )}
</span> </span>
</div> </div>
`; `;
@@ -393,9 +394,9 @@ export class AlbumDropdown extends LitElement {
style="height:${this.tracksHeight}px;column-count:${this.columnCount}" style="height:${this.tracksHeight}px;column-count:${this.columnCount}"
> >
${this.tracks.map( ${this.tracks.map(
(track, i) => (track, i) =>
this.renderTrackRow(track, i), this.renderTrackRow(track, i),
)} )}
</div> </div>
</div> </div>
`; `;
+102 -127
View File
@@ -81,11 +81,23 @@ export class CoverGrid extends LitElement {
private closeHandler = () => this.closeContextMenu(); private closeHandler = () => this.closeContextMenu();
private mousedownCloseHandler = (
e: MouseEvent,
) => {
const path = e.composedPath();
const popup = this.contextMenuPopup;
const submenu = this.playlistSubmenuPopup;
if (popup && path.includes(popup)) return;
if (submenu && path.includes(submenu)) return;
this.closeContextMenu();
};
/** /**
* When true, the next split→single transition * When true, the next split→single transition
* skips the expensive overlay capture and scroll * skips the expensive overlay capture and scroll
* restore. Used by the select-toggle checkbox * restore.
* so closing the dropdown is instant.
*/ */
private skipOverlay = false; private skipOverlay = false;
@@ -233,11 +245,6 @@ export class CoverGrid extends LitElement {
outline-offset: 2px; outline-offset: 2px;
} }
.album-card.expanded {
background-color: var(--yj-bg-elevated, #343a40);
border-radius: 8px 8px 0 0;
}
.album-card:focus-visible { .album-card:focus-visible {
outline: 2px solid var(--yj-accent, #ffd43b); outline: 2px solid var(--yj-accent, #ffd43b);
outline-offset: 2px; outline-offset: 2px;
@@ -257,45 +264,6 @@ export class CoverGrid extends LitElement {
scale: 0.95; scale: 0.95;
} }
/* ========================================
* Selection checkbox overlay
* ======================================== */
.select-toggle {
position: absolute;
top: 6px;
left: 6px;
width: 22px;
height: 22px;
border-radius: 50%;
border: 2px solid rgba(255, 255, 255, 0.8);
background-color: rgba(0, 0, 0, 0.4);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s ease;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.album-card:hover .select-toggle {
opacity: 1;
}
.select-toggle.checked {
opacity: 1;
background-color: var(--yj-accent, #ffd43b);
border-color: var(--yj-accent, #ffd43b);
}
.check-icon {
color: #000;
font-size: 14px;
font-weight: bold;
line-height: 1;
}
.cover-image { .cover-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
@@ -564,6 +532,10 @@ export class CoverGrid extends LitElement {
'contextmenu', 'contextmenu',
this.closeHandler, this.closeHandler,
); );
document.addEventListener(
'mousedown',
this.mousedownCloseHandler,
);
// error events do not bubble — use capture // error events do not bubble — use capture
// phase to catch <img> load failures. // phase to catch <img> load failures.
@@ -585,6 +557,10 @@ export class CoverGrid extends LitElement {
'contextmenu', 'contextmenu',
this.closeHandler, this.closeHandler,
); );
document.removeEventListener(
'mousedown',
this.mousedownCloseHandler,
);
this.removeEventListener( this.removeEventListener(
'error', 'error',
this.onGridImageError, this.onGridImageError,
@@ -2104,28 +2080,36 @@ export class CoverGrid extends LitElement {
* Dropdown (expand/collapse) * Dropdown (expand/collapse)
* ==================================================================== */ * ==================================================================== */
private async toggleDropdown( /** Close the dropdown if one is open. */
private closeDropdown() {
if (this.expandedAlbumId === null) return;
this.expandedAlbumId = null;
this.expandedTracks = [];
this.selectedTracks = new Set();
this.lastSelectedTrackIndex = null;
}
/**
* Open (or switch to) the given album's
* dropdown. If the dropdown is already open
* for the same album this is a no-op.
*/
private async openDropdown(
album: library.Album, album: library.Album,
) { ) {
if (this.expandedAlbumId === album.ID) { if (this.expandedAlbumId === album.ID) return;
// Close
this.expandedAlbumId = null;
this.expandedTracks = [];
this.selectedTracks = new Set();
this.lastSelectedTrackIndex = null;
return;
}
// Open (or switch)
this.expandedAlbumId = album.ID; this.expandedAlbumId = album.ID;
this.expandedTracks = []; this.expandedTracks = [];
this.selectedTracks = new Set(); this.selectedTracks = new Set();
this.lastSelectedTrackIndex = null; this.lastSelectedTrackIndex = null;
try {
const tracks = await GetAlbumTracks(album.ID);
// Only apply if still the same album try {
const tracks = await GetAlbumTracks(
album.ID,
);
if (this.expandedAlbumId === album.ID) { if (this.expandedAlbumId === album.ID) {
this.expandedTracks = tracks; this.expandedTracks = tracks;
} }
@@ -2137,14 +2121,25 @@ export class CoverGrid extends LitElement {
} }
} }
/** Close the dropdown if one is open. */ /**
private closeDropdown() { * Synchronise the dropdown to the current
if (this.expandedAlbumId === null) return; * selection: open the sole selected album's
* dropdown, or close it when zero or many
* albums are selected.
*/
private syncDropdownToSelection() {
if (this.selectedAlbums.size === 1) {
const [albumId] = this.selectedAlbums;
const album = this.filteredAlbums.find(
(a) => a.ID === albumId,
);
this.expandedAlbumId = null; if (album) {
this.expandedTracks = []; void this.openDropdown(album);
this.selectedTracks = new Set(); }
this.lastSelectedTrackIndex = null; } else {
this.closeDropdown();
}
} }
/* ==================================================================== /* ====================================================================
@@ -2211,7 +2206,7 @@ export class CoverGrid extends LitElement {
} }
this.selectedAlbums = next; this.selectedAlbums = next;
this.closeDropdown(); this.syncDropdownToSelection();
void this.warmAlbumFilePathCache(); void this.warmAlbumFilePathCache();
} else if (isCtrl) { } else if (isCtrl) {
const next = new Set(this.selectedAlbums); const next = new Set(this.selectedAlbums);
@@ -2224,12 +2219,28 @@ export class CoverGrid extends LitElement {
this.selectedAlbums = next; this.selectedAlbums = next;
this.lastSelectedAlbumIndex = index; this.lastSelectedAlbumIndex = index;
this.closeDropdown(); this.syncDropdownToSelection();
void this.warmAlbumFilePathCache(); void this.warmAlbumFilePathCache();
} else { } else {
this.selectedAlbums = new Set(); // Plain click: if this album is the
void this.toggleDropdown(album); // sole selection, deselect + close.
// Otherwise select only this album
// and open its dropdown.
if (
this.selectedAlbums.size === 1 &&
this.selectedAlbums.has(album.ID)
) {
this.selectedAlbums = new Set();
this.closeDropdown();
} else {
this.selectedAlbums = new Set([
album.ID,
]);
void this.openDropdown(album);
}
this.lastSelectedAlbumIndex = index; this.lastSelectedAlbumIndex = index;
void this.warmAlbumFilePathCache();
} }
}; };
@@ -2247,6 +2258,7 @@ export class CoverGrid extends LitElement {
if (filePaths.length === 0) return; if (filePaths.length === 0) return;
this.selectedAlbums = new Set(); this.selectedAlbums = new Set();
this.closeDropdown();
queueStore.setQueue(filePaths, 0); queueStore.setQueue(filePaths, 0);
}; };
@@ -2260,8 +2272,24 @@ export class CoverGrid extends LitElement {
if (!hit) return; if (!hit) return;
e.preventDefault(); e.preventDefault();
void this.toggleDropdown(hit.album);
this.lastSelectedAlbumIndex = hit.index; const { album, index } = hit;
// Mirror plain-click behaviour: toggle
// sole selection, or select and open.
if (
this.selectedAlbums.size === 1 &&
this.selectedAlbums.has(album.ID)
) {
this.selectedAlbums = new Set();
this.closeDropdown();
} else {
this.selectedAlbums = new Set([album.ID]);
void this.openDropdown(album);
}
this.lastSelectedAlbumIndex = index;
void this.warmAlbumFilePathCache();
}; };
private onGridAlbumContextMenu = ( private onGridAlbumContextMenu = (
@@ -2278,6 +2306,7 @@ export class CoverGrid extends LitElement {
this.selectedAlbums = new Set([ this.selectedAlbums = new Set([
hit.album.ID, hit.album.ID,
]); ]);
this.syncDropdownToSelection();
void this.warmAlbumFilePathCache(); void this.warmAlbumFilePathCache();
} }
@@ -2285,48 +2314,6 @@ export class CoverGrid extends LitElement {
this.openContextMenuAt(e.clientX, e.clientY); this.openContextMenuAt(e.clientX, e.clientY);
}; };
/**
* Selection checkbox toggle on album cards.
* Stops propagation so that the card click
* handler (which toggles the dropdown) does
* not fire.
*/
private onSelectToggleClick = (
e: MouseEvent,
) => {
e.stopPropagation();
const hit = this.resolveAlbumFromEvent(e);
if (!hit) return;
const { album, index } = hit;
const next = new Set(this.selectedAlbums);
if (next.has(album.ID)) {
next.delete(album.ID);
} else {
next.add(album.ID);
}
this.selectedAlbums = next;
this.lastSelectedAlbumIndex = index;
// Lightweight dropdown close: set the
// skipOverlay flag so willUpdate skips
// the expensive overlay capture and
// scroll-restore machinery.
if (this.expandedAlbumId !== null) {
this.skipOverlay = true;
this.expandedAlbumId = null;
this.expandedTracks = [];
this.selectedTracks = new Set();
this.lastSelectedTrackIndex = null;
}
void this.warmAlbumFilePathCache();
};
/** /**
* Delegated image error handler — falls back from * Delegated image error handler — falls back from
* thumbnail to full-size cover art. * thumbnail to full-size cover art.
@@ -2869,18 +2856,6 @@ export class CoverGrid extends LitElement {
> >
${this.getAlbumInitial(album.Name)} ${this.getAlbumInitial(album.Name)}
</div>`} </div>`}
<div
class="select-toggle ${selected ? 'checked' : ''}"
@click=${this.onSelectToggleClick}
>
${selected
? html`<span
class="check-icon"
>
&#10003;
</span>`
: nothing}
</div>
</div> </div>
<div class="album-info"> <div class="album-info">
<div <div
@@ -182,6 +182,23 @@ export class PlaylistView
this.closePlaylistContextMenu(); this.closePlaylistContextMenu();
}; };
private mousedownCloseHandler = (
e: MouseEvent,
) => {
const path = e.composedPath();
const popup = this.contextMenuPopup;
const submenu = this.playlistSubmenuPopup;
const plPopup =
this.playlistContextMenuPopup;
if (popup && path.includes(popup)) return;
if (submenu && path.includes(submenu)) return;
if (plPopup && path.includes(plPopup)) return;
this.closeContextMenu();
this.closePlaylistContextMenu();
};
private clearSelectionHandler = (e: MouseEvent) => { private clearSelectionHandler = (e: MouseEvent) => {
const path = e.composedPath(); const path = e.composedPath();
const isTrackClick = path.some( const isTrackClick = path.some(
@@ -664,6 +681,10 @@ export class PlaylistView
'contextmenu', 'contextmenu',
this.closeContextMenuHandler, this.closeContextMenuHandler,
); );
document.addEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.addEventListener( document.addEventListener(
'click', 'click',
this.clearSelectionHandler, this.clearSelectionHandler,
@@ -687,6 +708,10 @@ export class PlaylistView
'contextmenu', 'contextmenu',
this.closeContextMenuHandler, this.closeContextMenuHandler,
); );
document.removeEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.removeEventListener( document.removeEventListener(
'click', 'click',
this.clearSelectionHandler, this.clearSelectionHandler,
@@ -97,6 +97,17 @@ export class QueuePanel
private closeContextMenuHandler = () => private closeContextMenuHandler = () =>
this.closeContextMenu(); this.closeContextMenu();
private mousedownCloseHandler = (e: MouseEvent) => {
const path = e.composedPath();
const popup = this.contextMenuPopup;
const submenu = this.playlistSubmenuPopup;
if (popup && path.includes(popup)) return;
if (submenu && path.includes(submenu)) return;
this.closeContextMenu();
};
private clearSelectionHandler = (e: MouseEvent) => { private clearSelectionHandler = (e: MouseEvent) => {
const path = e.composedPath(); const path = e.composedPath();
const isTrackClick = path.some( const isTrackClick = path.some(
@@ -453,6 +464,10 @@ export class QueuePanel
'contextmenu', 'contextmenu',
this.closeContextMenuHandler, this.closeContextMenuHandler,
); );
document.addEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.addEventListener( document.addEventListener(
'click', 'click',
this.clearSelectionHandler, this.clearSelectionHandler,
@@ -485,6 +500,10 @@ export class QueuePanel
'contextmenu', 'contextmenu',
this.closeContextMenuHandler, this.closeContextMenuHandler,
); );
document.removeEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.removeEventListener( document.removeEventListener(
'click', 'click',
this.clearSelectionHandler, this.clearSelectionHandler,
@@ -66,6 +66,19 @@ export class TrackList extends LitElement implements SelectionHost {
private closeHandler = () => this.closeContextMenu(); private closeHandler = () => this.closeContextMenu();
private mousedownCloseHandler = (
e: MouseEvent,
) => {
const path = e.composedPath();
const popup = this.contextMenuPopup;
const submenu = this.playlistSubmenuPopup;
if (popup && path.includes(popup)) return;
if (submenu && path.includes(submenu)) return;
this.closeContextMenu();
};
private clearSelectionHandler = (e: MouseEvent) => { private clearSelectionHandler = (e: MouseEvent) => {
const path = e.composedPath(); const path = e.composedPath();
const isTrackClick = path.some( const isTrackClick = path.some(
@@ -437,6 +450,7 @@ export class TrackList extends LitElement implements SelectionHost {
); );
document.addEventListener('click', this.closeHandler); document.addEventListener('click', this.closeHandler);
document.addEventListener('contextmenu', this.closeHandler); document.addEventListener('contextmenu', this.closeHandler);
document.addEventListener('mousedown', this.mousedownCloseHandler);
document.addEventListener('click', this.clearSelectionHandler); document.addEventListener('click', this.clearSelectionHandler);
document.addEventListener('mousemove', this.onColResizeMove); document.addEventListener('mousemove', this.onColResizeMove);
document.addEventListener('mouseup', this.onColResizeEnd); document.addEventListener('mouseup', this.onColResizeEnd);
@@ -458,6 +472,7 @@ export class TrackList extends LitElement implements SelectionHost {
this.cancelScanComplete?.(); this.cancelScanComplete?.();
document.removeEventListener('click', this.closeHandler); document.removeEventListener('click', this.closeHandler);
document.removeEventListener('contextmenu', this.closeHandler); document.removeEventListener('contextmenu', this.closeHandler);
document.removeEventListener('mousedown', this.mousedownCloseHandler);
document.removeEventListener('click', this.clearSelectionHandler); document.removeEventListener('click', this.clearSelectionHandler);
document.removeEventListener('mousemove', this.onColResizeMove); document.removeEventListener('mousemove', this.onColResizeMove);
document.removeEventListener('mouseup', this.onColResizeEnd); document.removeEventListener('mouseup', this.onColResizeEnd);