cleaned up album selection behavior, context menu now closes when clicking outside of it
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
✓
|
||||
</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);
|
||||
|
||||
Reference in New Issue
Block a user