diff --git a/frontend/src/components/cover-grid/album-dropdown.ts b/frontend/src/components/cover-grid/album-dropdown.ts
index 4edaa59..5d270f0 100644
--- a/frontend/src/components/cover-grid/album-dropdown.ts
+++ b/frontend/src/components/cover-grid/album-dropdown.ts
@@ -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()}
>
@@ -371,8 +372,8 @@ export class AlbumDropdown extends LitElement {
${formatMilliseconds(
- track.TrackLength,
- )}
+ track.TrackLength,
+ )}
`;
@@ -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),
+ )}
`;
diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts
index 02beb1a..ab11670 100644
--- a/frontend/src/components/cover-grid/cover-grid.ts
+++ b/frontend/src/components/cover-grid/cover-grid.ts
@@ -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 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)}
`}
-