fixed cover-grid selection and queuing behavior, added "clear queue" button
This commit is contained in:
@@ -53,17 +53,35 @@ export class AlbumDropdown extends LitElement {
|
||||
@property({ type: Number })
|
||||
containerWidth = 800;
|
||||
|
||||
/** Width of the album row in pixels (cards + gaps, no outer padding). */
|
||||
@property({ type: Number })
|
||||
gridRowWidth = 800;
|
||||
|
||||
/** Horizontal offset of the carat from the dropdown's left edge. */
|
||||
@property({ type: Number })
|
||||
caratOffset = 0;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.album-dropdown {
|
||||
background-color: #212529;
|
||||
border: 2px solid #ffd43b;
|
||||
border-radius: 4px;
|
||||
background-color: #343a40;
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 12px 16px;
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.carat {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 9px solid transparent;
|
||||
border-right: 9px solid transparent;
|
||||
border-bottom: 8px solid #343a40;
|
||||
}
|
||||
|
||||
.dropdown-tracks {
|
||||
@@ -366,7 +384,14 @@ export class AlbumDropdown extends LitElement {
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<div class="album-dropdown">
|
||||
<div
|
||||
class="album-dropdown"
|
||||
style="width:${this.gridRowWidth}px"
|
||||
>
|
||||
<div
|
||||
class="carat"
|
||||
style="left:${this.caratOffset}px;transform:translateX(-50%)"
|
||||
></div>
|
||||
<div
|
||||
class="dropdown-tracks"
|
||||
style="height:${this.tracksHeight}px;column-count:${this.columnCount}"
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from '@utils/drag-controller';
|
||||
import type { DragPayload } from '@utils/drag-controller';
|
||||
import {
|
||||
createAlbumArtDragImage,
|
||||
createDragImage,
|
||||
removeDragImage,
|
||||
} from '@utils/drag-image';
|
||||
@@ -77,6 +78,14 @@ export class CoverGrid extends LitElement {
|
||||
|
||||
private closeHandler = () => 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.
|
||||
*/
|
||||
private skipOverlay = false;
|
||||
|
||||
/** Current card width — driven by the store. */
|
||||
private get cardWidth(): number {
|
||||
return this.libraryCtrl.coverSize;
|
||||
@@ -162,8 +171,7 @@ export class CoverGrid extends LitElement {
|
||||
private wheelListenerAttached = false;
|
||||
|
||||
// buildGridEntries() memoization cache.
|
||||
private entriesCacheAlbums: library.Album[] = [];
|
||||
private entriesCache: GridEntry[] = [];
|
||||
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
@@ -204,8 +212,8 @@ export class CoverGrid extends LitElement {
|
||||
}
|
||||
|
||||
.album-card.expanded {
|
||||
outline: 2px solid #ffd43b;
|
||||
outline-offset: 2px;
|
||||
background-color: #343a40;
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
.album-card:focus-visible {
|
||||
@@ -220,12 +228,57 @@ export class CoverGrid extends LitElement {
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
background-color: #282828;
|
||||
transition: scale 0.15s ease;
|
||||
}
|
||||
|
||||
.album-card.selected .cover-container {
|
||||
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: #ffd43b;
|
||||
border-color: #ffd43b;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
color: #000;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.cover-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.placeholder-cover {
|
||||
@@ -247,6 +300,11 @@ export class CoverGrid extends LitElement {
|
||||
margin-top: 4px;
|
||||
min-width: 0;
|
||||
text-align: center;
|
||||
transition: scale 0.15s ease;
|
||||
}
|
||||
|
||||
.album-card.selected .album-info {
|
||||
scale: 0.95;
|
||||
}
|
||||
|
||||
.album-name {
|
||||
@@ -545,110 +603,124 @@ export class CoverGrid extends LitElement {
|
||||
this.expandedTracks.length === 0 &&
|
||||
this.splitMode
|
||||
) {
|
||||
// Capture the raw split-mode scrollTop
|
||||
// before converting to single-mode coords.
|
||||
const rawScrollTop =
|
||||
this.scrollContainer?.scrollTop ?? 0;
|
||||
if (this.skipOverlay) {
|
||||
// Lightweight exit: skip the
|
||||
// overlay capture and scroll
|
||||
// restore so the transition is
|
||||
// instant.
|
||||
this.skipOverlay = false;
|
||||
this.splitMode = false;
|
||||
} else {
|
||||
// Capture the raw split-mode scrollTop
|
||||
// before converting to single-mode coords.
|
||||
const rawScrollTop =
|
||||
this.scrollContainer?.scrollTop ??
|
||||
0;
|
||||
|
||||
// Convert to dropdown-free coordinates
|
||||
// before exiting split mode.
|
||||
this.savedScrollTop =
|
||||
this.computeAdjustedScrollTop();
|
||||
// Convert to dropdown-free coordinates
|
||||
// before exiting split mode.
|
||||
this.savedScrollTop =
|
||||
this.computeAdjustedScrollTop();
|
||||
|
||||
// If switching to a new album (not
|
||||
// closing), record the viewport offset
|
||||
// of the newly-expanded album in the
|
||||
// OLD split layout so the enter-split
|
||||
// restore can place it at the same
|
||||
// visual position.
|
||||
if (this.expandedAlbumId !== null) {
|
||||
const idx = this.albums.findIndex(
|
||||
(a) =>
|
||||
a.ID ===
|
||||
this.expandedAlbumId,
|
||||
// If switching to a new album (not
|
||||
// closing), record the viewport offset
|
||||
// of the newly-expanded album in the
|
||||
// OLD split layout so the enter-split
|
||||
// restore can place it at the same
|
||||
// visual position.
|
||||
if (this.expandedAlbumId !== null) {
|
||||
const idx = this.albums.findIndex(
|
||||
(a) =>
|
||||
a.ID ===
|
||||
this.expandedAlbumId,
|
||||
);
|
||||
|
||||
if (idx >= 0) {
|
||||
const gap =
|
||||
CoverGrid.GRID_GAP;
|
||||
const pad =
|
||||
CoverGrid.GRID_PADDING;
|
||||
const cols =
|
||||
this.getColumnCount();
|
||||
const rowStep =
|
||||
this.cardHeight + gap;
|
||||
const row = Math.floor(
|
||||
idx / cols,
|
||||
);
|
||||
|
||||
// Album's Y in single-mode
|
||||
// (no dropdown) coordinates.
|
||||
const albumY =
|
||||
pad + row * rowStep;
|
||||
|
||||
// In the old split layout
|
||||
// the dropdown shifts
|
||||
// everything below it.
|
||||
const oldBeforeRows =
|
||||
Math.ceil(
|
||||
this.splitIndex /
|
||||
cols,
|
||||
);
|
||||
const oldDropdownTop =
|
||||
pad +
|
||||
oldBeforeRows * rowStep;
|
||||
const dropdown =
|
||||
this.shadowRoot?.querySelector(
|
||||
'album-dropdown',
|
||||
);
|
||||
const oldDropdownHeight =
|
||||
(
|
||||
dropdown as HTMLElement
|
||||
)?.offsetHeight ?? 0;
|
||||
|
||||
const albumYOldSplit =
|
||||
albumY >= oldDropdownTop
|
||||
? albumY +
|
||||
oldDropdownHeight
|
||||
: albumY;
|
||||
|
||||
// Viewport offset in
|
||||
// old-split coordinates.
|
||||
this.savedAlbumViewportOffset =
|
||||
albumYOldSplit -
|
||||
rawScrollTop;
|
||||
|
||||
console.log(
|
||||
'[willUpdate] anchor capture',
|
||||
{
|
||||
albumY,
|
||||
oldDropdownTop,
|
||||
oldDropdownHeight,
|
||||
albumYOldSplit,
|
||||
rawScrollTop,
|
||||
offset: this
|
||||
.savedAlbumViewportOffset,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.savedAlbumViewportOffset =
|
||||
null;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[willUpdate] exit split (tracks empty)',
|
||||
{
|
||||
savedScrollTop:
|
||||
this.savedScrollTop,
|
||||
savedAlbumViewportOffset:
|
||||
this
|
||||
.savedAlbumViewportOffset,
|
||||
expandedAlbumId:
|
||||
this.expandedAlbumId,
|
||||
},
|
||||
);
|
||||
|
||||
if (idx >= 0) {
|
||||
const gap = CoverGrid.GRID_GAP;
|
||||
const pad =
|
||||
CoverGrid.GRID_PADDING;
|
||||
const cols =
|
||||
this.getColumnCount();
|
||||
const rowStep =
|
||||
this.cardHeight + gap;
|
||||
const row = Math.floor(
|
||||
idx / cols,
|
||||
);
|
||||
|
||||
// Album's Y in single-mode
|
||||
// (no dropdown) coordinates.
|
||||
const albumY =
|
||||
pad + row * rowStep;
|
||||
|
||||
// In the old split layout the
|
||||
// dropdown shifts everything
|
||||
// below it. Determine if
|
||||
// album B sits below the old
|
||||
// dropdown.
|
||||
const oldBeforeRows = Math.ceil(
|
||||
this.splitIndex / cols,
|
||||
);
|
||||
const oldDropdownTop =
|
||||
pad + oldBeforeRows * rowStep;
|
||||
const dropdown =
|
||||
this.shadowRoot?.querySelector(
|
||||
'album-dropdown',
|
||||
);
|
||||
const oldDropdownHeight =
|
||||
(dropdown as HTMLElement)
|
||||
?.offsetHeight ?? 0;
|
||||
|
||||
const albumYOldSplit =
|
||||
albumY >= oldDropdownTop
|
||||
? albumY +
|
||||
oldDropdownHeight
|
||||
: albumY;
|
||||
|
||||
// Viewport offset in old-split
|
||||
// coordinates: use the raw
|
||||
// (un-adjusted) scrollTop.
|
||||
this.savedAlbumViewportOffset =
|
||||
albumYOldSplit - rawScrollTop;
|
||||
|
||||
console.log(
|
||||
'[willUpdate] anchor capture',
|
||||
{
|
||||
albumY,
|
||||
oldDropdownTop,
|
||||
oldDropdownHeight,
|
||||
albumYOldSplit,
|
||||
rawScrollTop,
|
||||
offset: this
|
||||
.savedAlbumViewportOffset,
|
||||
},
|
||||
);
|
||||
}
|
||||
} else {
|
||||
this.savedAlbumViewportOffset =
|
||||
null;
|
||||
this.captureOverlay();
|
||||
this.splitMode = false;
|
||||
this.needsScrollRestore = true;
|
||||
this.showDropdownAfterRestore = false;
|
||||
}
|
||||
|
||||
console.log(
|
||||
'[willUpdate] exit split (tracks empty)',
|
||||
{
|
||||
savedScrollTop:
|
||||
this.savedScrollTop,
|
||||
savedAlbumViewportOffset:
|
||||
this.savedAlbumViewportOffset,
|
||||
expandedAlbumId:
|
||||
this.expandedAlbumId,
|
||||
},
|
||||
);
|
||||
|
||||
this.captureOverlay();
|
||||
this.splitMode = false;
|
||||
this.needsScrollRestore = true;
|
||||
this.showDropdownAfterRestore = false;
|
||||
}
|
||||
|
||||
// Enter split mode when tracks have loaded.
|
||||
@@ -1360,6 +1432,46 @@ export class CoverGrid extends LitElement {
|
||||
return el?.clientWidth ?? 800;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the album row: from the left edge of
|
||||
* the leftmost card to the right edge of the
|
||||
* rightmost card, including card padding but not
|
||||
* the outer grid padding.
|
||||
*/
|
||||
private getGridRowWidth(): number {
|
||||
const cols = this.getColumnCount();
|
||||
const gap = CoverGrid.GRID_GAP;
|
||||
|
||||
return (
|
||||
cols * this.cardWidth +
|
||||
(cols - 1) * gap
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Horizontal offset of the carat (in pixels from
|
||||
* the left edge of the dropdown) so that it points
|
||||
* at the center of the expanded album card.
|
||||
*/
|
||||
private getCaratOffset(): number {
|
||||
if (this.expandedAlbumId === null) return 0;
|
||||
|
||||
const idx = this.albums.findIndex(
|
||||
(a) => a.ID === this.expandedAlbumId,
|
||||
);
|
||||
|
||||
if (idx < 0) return 0;
|
||||
|
||||
const cols = this.getColumnCount();
|
||||
const colIndex = idx % cols;
|
||||
const gap = CoverGrid.GRID_GAP;
|
||||
|
||||
return (
|
||||
colIndex * (this.cardWidth + gap) +
|
||||
this.cardWidth / 2
|
||||
);
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Split-mode helpers
|
||||
*
|
||||
@@ -1406,14 +1518,11 @@ export class CoverGrid extends LitElement {
|
||||
|
||||
/**
|
||||
* Build a flat GridEntry array for a given album
|
||||
* slice. Used by both single and split modes.
|
||||
* slice. Always returns a new array so the
|
||||
* virtualizer re-renders visible items when
|
||||
* component state (e.g. selectedAlbums) changes.
|
||||
*/
|
||||
private buildGridEntries(): GridEntry[] {
|
||||
// Return cached result when input unchanged.
|
||||
if (this.entriesCacheAlbums === this.albums) {
|
||||
return this.entriesCache;
|
||||
}
|
||||
|
||||
const entries: GridEntry[] = [];
|
||||
|
||||
for (let i = 0; i < this.albums.length; i++) {
|
||||
@@ -1423,9 +1532,6 @@ export class CoverGrid extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
this.entriesCacheAlbums = this.albums;
|
||||
this.entriesCache = entries;
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
@@ -1868,13 +1974,6 @@ export class CoverGrid extends LitElement {
|
||||
this.selectedAlbums.has(a.ID),
|
||||
);
|
||||
|
||||
// Prune stale entries.
|
||||
for (const id of this.albumFilePathCache.keys()) {
|
||||
if (!this.selectedAlbums.has(id)) {
|
||||
this.albumFilePathCache.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch missing entries.
|
||||
for (const album of selected) {
|
||||
if (this.albumFilePathCache.has(album.ID)) {
|
||||
@@ -1992,6 +2091,16 @@ export class CoverGrid extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/* ====================================================================
|
||||
* Event delegation helpers
|
||||
* ==================================================================== */
|
||||
@@ -2055,6 +2164,7 @@ export class CoverGrid extends LitElement {
|
||||
}
|
||||
|
||||
this.selectedAlbums = next;
|
||||
this.closeDropdown();
|
||||
void this.warmAlbumFilePathCache();
|
||||
} else if (isCtrl) {
|
||||
const next = new Set(this.selectedAlbums);
|
||||
@@ -2067,8 +2177,10 @@ export class CoverGrid extends LitElement {
|
||||
|
||||
this.selectedAlbums = next;
|
||||
this.lastSelectedAlbumIndex = index;
|
||||
this.closeDropdown();
|
||||
void this.warmAlbumFilePathCache();
|
||||
} else {
|
||||
this.selectedAlbums = new Set();
|
||||
void this.toggleDropdown(album);
|
||||
this.lastSelectedAlbumIndex = index;
|
||||
}
|
||||
@@ -2126,6 +2238,48 @@ 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.
|
||||
@@ -2297,6 +2451,36 @@ export class CoverGrid extends LitElement {
|
||||
* Drag source (album cards)
|
||||
* ==================================================================== */
|
||||
|
||||
/**
|
||||
* Pre-warm the file-path cache for the album under
|
||||
* the pointer so that a subsequent dragstart (which
|
||||
* is synchronous) can read the paths immediately.
|
||||
*/
|
||||
private onAlbumPointerDown = (e: PointerEvent) => {
|
||||
// Only act on primary button (left-click).
|
||||
if (e.button !== 0) return;
|
||||
|
||||
const hit = this.resolveAlbumFromEvent(e);
|
||||
|
||||
if (!hit) return;
|
||||
|
||||
if (this.albumFilePathCache.has(hit.album.ID)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire-and-forget: warm the cache entry.
|
||||
void this.getAlbumFilePaths(hit.album).then(
|
||||
(paths) => {
|
||||
if (paths.length > 0) {
|
||||
this.albumFilePathCache.set(
|
||||
hit.album.ID,
|
||||
paths,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
private onAlbumDragStart = (e: DragEvent) => {
|
||||
const hit = this.resolveAlbumFromEvent(e);
|
||||
|
||||
@@ -2304,20 +2488,26 @@ export class CoverGrid extends LitElement {
|
||||
|
||||
// Read file paths synchronously from the
|
||||
// pre-warmed cache. The cache is populated
|
||||
// asynchronously whenever the album selection
|
||||
// changes, so by the time the user drags, the
|
||||
// data is already available.
|
||||
// via pointerdown or selection-change warming.
|
||||
let filePaths: string[];
|
||||
let isSingleAlbum: boolean;
|
||||
|
||||
if (this.selectedAlbums.has(hit.album.ID)) {
|
||||
// Dragged album is part of the selection —
|
||||
// drag all selected albums' tracks.
|
||||
filePaths =
|
||||
this.getCachedSelectedAlbumFilePaths();
|
||||
isSingleAlbum =
|
||||
this.selectedAlbums.size === 1;
|
||||
} else {
|
||||
// Single unselected album — check cache.
|
||||
// Dragged album is not selected — clear
|
||||
// selection and drag only this album.
|
||||
this.selectedAlbums = new Set();
|
||||
filePaths =
|
||||
this.albumFilePathCache.get(
|
||||
hit.album.ID,
|
||||
) ?? [];
|
||||
isSingleAlbum = true;
|
||||
}
|
||||
|
||||
if (filePaths.length === 0) {
|
||||
@@ -2332,9 +2522,19 @@ export class CoverGrid extends LitElement {
|
||||
source: 'cover-grid',
|
||||
});
|
||||
|
||||
this.dragImageEl = createDragImage(
|
||||
filePaths.length,
|
||||
);
|
||||
// Single album: show cover art thumbnail.
|
||||
// Multiple albums: show track-count badge.
|
||||
if (isSingleAlbum && hit.album.CoverArtPath) {
|
||||
this.dragImageEl =
|
||||
createAlbumArtDragImage(
|
||||
this.getCoverUrl(hit.album),
|
||||
);
|
||||
} else {
|
||||
this.dragImageEl = createDragImage(
|
||||
filePaths.length,
|
||||
);
|
||||
}
|
||||
|
||||
e.dataTransfer?.setDragImage(
|
||||
this.dragImageEl,
|
||||
0,
|
||||
@@ -2602,6 +2802,7 @@ export class CoverGrid extends LitElement {
|
||||
data-index=${index}
|
||||
aria-label="${album.Name} by ${album.ArtistName}"
|
||||
draggable="true"
|
||||
@pointerdown=${this.onAlbumPointerDown}
|
||||
@dragstart=${this.onAlbumDragStart}
|
||||
@dragend=${this.onAlbumDragEnd}
|
||||
>
|
||||
@@ -2621,6 +2822,18 @@ 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
|
||||
@@ -2725,6 +2938,9 @@ export class CoverGrid extends LitElement {
|
||||
.tracks=${this.expandedTracks}
|
||||
.selectedTracks=${this.selectedTracks}
|
||||
.containerWidth=${this.getContainerWidth()}
|
||||
.gridRowWidth=${this.getGridRowWidth()}
|
||||
.caratOffset=${this.getCaratOffset()}
|
||||
style="margin-left:${(this.getContainerWidth() - this.getGridRowWidth()) / 2}px"
|
||||
@track-click=${this.onTrackClick}
|
||||
@track-dblclick=${this.onTrackDblClick}
|
||||
@track-contextmenu=${this.onTrackContextMenu}
|
||||
|
||||
@@ -206,7 +206,13 @@ export class QueuePanel
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.add-to-playlist-button {
|
||||
.header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.header-action-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
@@ -216,11 +222,11 @@ export class QueuePanel
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.add-to-playlist-button:hover {
|
||||
.header-action-button:hover {
|
||||
color: #ffd43b;
|
||||
}
|
||||
|
||||
.add-to-playlist-button:disabled {
|
||||
.header-action-button:disabled {
|
||||
color: #555;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -515,6 +521,10 @@ export class QueuePanel
|
||||
}
|
||||
}
|
||||
|
||||
private handleClearQueue = () => {
|
||||
this.queue.clearQueue();
|
||||
};
|
||||
|
||||
private async handleAddToPlaylist() {
|
||||
if (this.queue.tracks.length === 0) return;
|
||||
|
||||
@@ -1182,14 +1192,28 @@ export class QueuePanel
|
||||
></div>
|
||||
<div class="header">
|
||||
<h3>Queue</h3>
|
||||
<button
|
||||
class="add-to-playlist-button"
|
||||
@click=${this.handleAddToPlaylist}
|
||||
?disabled=${tracks.length === 0}
|
||||
title="Add queue to playlist"
|
||||
>
|
||||
<wa-icon name="plus"></wa-icon>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button
|
||||
class="header-action-button"
|
||||
@click=${this.handleClearQueue}
|
||||
?disabled=${tracks.length === 0}
|
||||
title="Clear queue"
|
||||
>
|
||||
<wa-icon
|
||||
name="trash"
|
||||
></wa-icon>
|
||||
</button>
|
||||
<button
|
||||
class="header-action-button add-to-playlist-button"
|
||||
@click=${this.handleAddToPlaylist}
|
||||
?disabled=${tracks.length === 0}
|
||||
title="Add queue to playlist"
|
||||
>
|
||||
<wa-icon
|
||||
name="plus"
|
||||
></wa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<wa-popup
|
||||
|
||||
Reference in New Issue
Block a user