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`
:host {
display: block;
margin-top: 14px;
}
.album-dropdown {
@@ -76,12 +77,12 @@ export class AlbumDropdown extends LitElement {
.carat {
position: absolute;
top: -8px;
top: -10px;
width: 0;
height: 0;
border-left: 9px solid transparent;
border-right: 9px solid transparent;
border-bottom: 8px solid var(--yj-bg-elevated, #343a40);
border-left: 11px solid transparent;
border-right: 11px solid transparent;
border-bottom: 10px solid var(--yj-bg-elevated, #343a40);
}
.dropdown-tracks {
@@ -343,21 +344,21 @@ export class AlbumDropdown extends LitElement {
class=${classes}
draggable="true"
@click=${(e: MouseEvent) =>
this.onTrackClick(e, track, index)}
this.onTrackClick(e, track, index)}
@dblclick=${(e: MouseEvent) =>
this.onTrackDblClick(
e,
track,
index,
)}
this.onTrackDblClick(
e,
track,
index,
)}
@contextmenu=${(e: MouseEvent) =>
this.onTrackContextMenu(e, track)}
this.onTrackContextMenu(e, track)}
@dragstart=${(e: DragEvent) =>
this.onTrackDragStart(
e,
track,
index,
)}
this.onTrackDragStart(
e,
track,
index,
)}
@dragend=${() => this.onTrackDragEnd()}
>
<span class="track-number">
@@ -371,8 +372,8 @@ export class AlbumDropdown extends LitElement {
</span>
<span class="track-duration">
${formatMilliseconds(
track.TrackLength,
)}
track.TrackLength,
)}
</span>
</div>
`;
@@ -393,9 +394,9 @@ export class AlbumDropdown extends LitElement {
style="height:${this.tracksHeight}px;column-count:${this.columnCount}"
>
${this.tracks.map(
(track, i) =>
this.renderTrackRow(track, i),
)}
(track, i) =>
this.renderTrackRow(track, i),
)}
</div>
</div>
`;
+102 -127
View File
@@ -81,11 +81,23 @@ export class CoverGrid extends LitElement {
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
* skips the expensive overlay capture and scroll
* restore. Used by the select-toggle checkbox
* so closing the dropdown is instant.
* restore.
*/
private skipOverlay = false;
@@ -233,11 +245,6 @@ export class CoverGrid extends LitElement {
outline-offset: 2px;
}
.album-card.expanded {
background-color: var(--yj-bg-elevated, #343a40);
border-radius: 8px 8px 0 0;
}
.album-card:focus-visible {
outline: 2px solid var(--yj-accent, #ffd43b);
outline-offset: 2px;
@@ -257,45 +264,6 @@ export class CoverGrid extends LitElement {
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 {
width: 100%;
height: 100%;
@@ -564,6 +532,10 @@ export class CoverGrid extends LitElement {
'contextmenu',
this.closeHandler,
);
document.addEventListener(
'mousedown',
this.mousedownCloseHandler,
);
// error events do not bubble — use capture
// phase to catch <img> load failures.
@@ -585,6 +557,10 @@ export class CoverGrid extends LitElement {
'contextmenu',
this.closeHandler,
);
document.removeEventListener(
'mousedown',
this.mousedownCloseHandler,
);
this.removeEventListener(
'error',
this.onGridImageError,
@@ -2104,28 +2080,36 @@ export class CoverGrid extends LitElement {
* 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,
) {
if (this.expandedAlbumId === album.ID) {
// Close
this.expandedAlbumId = null;
this.expandedTracks = [];
this.selectedTracks = new Set();
this.lastSelectedTrackIndex = null;
if (this.expandedAlbumId === album.ID) return;
return;
}
// Open (or switch)
this.expandedAlbumId = album.ID;
this.expandedTracks = [];
this.selectedTracks = new Set();
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) {
this.expandedTracks = tracks;
}
@@ -2137,14 +2121,25 @@ export class CoverGrid extends LitElement {
}
}
/** Close the dropdown if one is open. */
private closeDropdown() {
if (this.expandedAlbumId === null) return;
/**
* Synchronise the dropdown to the current
* 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;
this.expandedTracks = [];
this.selectedTracks = new Set();
this.lastSelectedTrackIndex = null;
if (album) {
void this.openDropdown(album);
}
} else {
this.closeDropdown();
}
}
/* ====================================================================
@@ -2211,7 +2206,7 @@ export class CoverGrid extends LitElement {
}
this.selectedAlbums = next;
this.closeDropdown();
this.syncDropdownToSelection();
void this.warmAlbumFilePathCache();
} else if (isCtrl) {
const next = new Set(this.selectedAlbums);
@@ -2224,12 +2219,28 @@ export class CoverGrid extends LitElement {
this.selectedAlbums = next;
this.lastSelectedAlbumIndex = index;
this.closeDropdown();
this.syncDropdownToSelection();
void this.warmAlbumFilePathCache();
} else {
this.selectedAlbums = new Set();
void this.toggleDropdown(album);
// Plain click: if this album is the
// 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;
void this.warmAlbumFilePathCache();
}
};
@@ -2247,6 +2258,7 @@ export class CoverGrid extends LitElement {
if (filePaths.length === 0) return;
this.selectedAlbums = new Set();
this.closeDropdown();
queueStore.setQueue(filePaths, 0);
};
@@ -2260,8 +2272,24 @@ export class CoverGrid extends LitElement {
if (!hit) return;
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 = (
@@ -2278,6 +2306,7 @@ export class CoverGrid extends LitElement {
this.selectedAlbums = new Set([
hit.album.ID,
]);
this.syncDropdownToSelection();
void this.warmAlbumFilePathCache();
}
@@ -2285,48 +2314,6 @@ export class CoverGrid extends LitElement {
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
* thumbnail to full-size cover art.
@@ -2869,18 +2856,6 @@ export class CoverGrid extends LitElement {
>
${this.getAlbumInitial(album.Name)}
</div>`}
<div
class="select-toggle ${selected ? 'checked' : ''}"
@click=${this.onSelectToggleClick}
>
${selected
? html`<span
class="check-icon"
>
&#10003;
</span>`
: nothing}
</div>
</div>
<div class="album-info">
<div
@@ -182,6 +182,23 @@ export class PlaylistView
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) => {
const path = e.composedPath();
const isTrackClick = path.some(
@@ -664,6 +681,10 @@ export class PlaylistView
'contextmenu',
this.closeContextMenuHandler,
);
document.addEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.addEventListener(
'click',
this.clearSelectionHandler,
@@ -687,6 +708,10 @@ export class PlaylistView
'contextmenu',
this.closeContextMenuHandler,
);
document.removeEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.removeEventListener(
'click',
this.clearSelectionHandler,
@@ -97,6 +97,17 @@ export class QueuePanel
private closeContextMenuHandler = () =>
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) => {
const path = e.composedPath();
const isTrackClick = path.some(
@@ -453,6 +464,10 @@ export class QueuePanel
'contextmenu',
this.closeContextMenuHandler,
);
document.addEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.addEventListener(
'click',
this.clearSelectionHandler,
@@ -485,6 +500,10 @@ export class QueuePanel
'contextmenu',
this.closeContextMenuHandler,
);
document.removeEventListener(
'mousedown',
this.mousedownCloseHandler,
);
document.removeEventListener(
'click',
this.clearSelectionHandler,
@@ -66,6 +66,19 @@ export class TrackList extends LitElement implements SelectionHost {
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) => {
const path = e.composedPath();
const isTrackClick = path.some(
@@ -437,6 +450,7 @@ export class TrackList extends LitElement implements SelectionHost {
);
document.addEventListener('click', this.closeHandler);
document.addEventListener('contextmenu', this.closeHandler);
document.addEventListener('mousedown', this.mousedownCloseHandler);
document.addEventListener('click', this.clearSelectionHandler);
document.addEventListener('mousemove', this.onColResizeMove);
document.addEventListener('mouseup', this.onColResizeEnd);
@@ -458,6 +472,7 @@ export class TrackList extends LitElement implements SelectionHost {
this.cancelScanComplete?.();
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('contextmenu', this.closeHandler);
document.removeEventListener('mousedown', this.mousedownCloseHandler);
document.removeEventListener('click', this.clearSelectionHandler);
document.removeEventListener('mousemove', this.onColResizeMove);
document.removeEventListener('mouseup', this.onColResizeEnd);