cover grid refactor

-split component into several files
This commit is contained in:
2026-02-24 09:49:34 -05:00
parent 352fa9da61
commit 1221a403cf
13 changed files with 3055 additions and 3193 deletions
+39 -190
View File
@@ -9,6 +9,11 @@ import {
import { EventsOn } from '@runtime/runtime';
import { SelectionController } from '@utils/selection-controller';
import type { SelectionHost } from '@utils/selection-controller';
import {
ContextMenuController,
contextMenuStyles,
} from '@utils/context-menu-controller.js';
import type { ContextMenuHost } from '@utils/context-menu-controller.js';
import { PlayerController } from '@store/controllers/player-controller';
import { SearchController } from '@store/controllers/search-controller';
import { TrackListController } from '@store/controllers/tracklist-controller';
@@ -39,7 +44,6 @@ import '@awesome.me/webawesome/dist/components/popup/popup.js';
import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js';
import '@awesome.me/webawesome/dist/components/icon/icon.js';
import '@components/playlist-picker/playlist-picker.js';
import type { PlaylistPicker } from '@components/playlist-picker/playlist-picker.js';
import '@components/track-details/track-details.js';
import type { TrackDetails } from '@components/track-details/track-details.js';
import type { CoverArtUrls } from '@components/track-details/track-details.js';
@@ -53,7 +57,7 @@ const DEFAULT_FIXED_WIDTH = 80;
type SortDirection = 'asc' | 'desc';
@customElement('track-list')
export class TrackList extends LitElement implements SelectionHost {
export class TrackList extends LitElement implements SelectionHost, ContextMenuHost {
/**
* When set, the list displays these tracks instead of
* fetching all tracks from the library store. The
@@ -68,6 +72,7 @@ export class TrackList extends LitElement implements SelectionHost {
private searchCtrl = new SearchController(this);
private trackListCtrl = new TrackListController(this);
private selection = new SelectionController(this);
private ctxMenu = new ContextMenuController(this);
private cancelScanComplete?: () => void;
private lastSearchTerm = '';
@@ -98,18 +103,22 @@ export class TrackList extends LitElement implements SelectionHost {
@state()
private tracks: library.Track[] = [];
@state()
private contextMenuOpen = false;
@state()
private playlistSubmenuOpen = false;
@query('#context-menu')
private contextMenuPopup!: HTMLElement;
@query('#playlist-submenu')
private playlistSubmenuPopup!: HTMLElement;
// -- ContextMenuHost interface --
getContextMenuPopup(): HTMLElement | undefined {
return this.contextMenuPopup;
}
getPlaylistSubmenuPopup(): HTMLElement | undefined {
return this.playlistSubmenuPopup;
}
@query('track-details')
private trackDetailsDialog!: TrackDetails;
@@ -118,10 +127,6 @@ export class TrackList extends LitElement implements SelectionHost {
private lastActiveTrackPath: string | null = null;
private submenuCloseTimer: ReturnType<
typeof setTimeout
> | null = null;
// -- Memoisation caches for filtered / sorted tracks --
private cachedFilteredTracks: library.Track[] = [];
private cachedSortedTracks: library.Track[] = [];
@@ -132,21 +137,6 @@ export class TrackList extends LitElement implements SelectionHost {
private prevSortField: string | null = null;
private prevSortDir: SortDirection = 'asc';
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(
@@ -671,7 +661,7 @@ export class TrackList extends LitElement implements SelectionHost {
this.requestUpdate();
};
static override styles = css`
static override styles = [contextMenuStyles, css`
:host {
display: flex;
flex-direction: column;
@@ -942,46 +932,7 @@ export class TrackList extends LitElement implements SelectionHost {
text-align: center;
}
#context-menu {
z-index: 200;
}
.context-menu-panel {
background-color: var(--yj-bg-elevated, #343a40);
border: 1px solid var(--yj-border, #444);
border-radius: 6px;
padding: 4px 0;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
min-width: 160px;
}
.context-menu-panel wa-dropdown-item {
cursor: pointer;
}
.context-menu-panel wa-dropdown-item {
--wa-color-text-normal: var(--yj-text-primary, #fff);
font-size: 13px;
}
.context-menu-panel wa-dropdown-item:hover {
background-color: var(--yj-hover-overlay, rgba(255, 255, 255, 0.1));
}
.submenu-item {
position: relative;
}
.submenu-arrow {
font-size: 10px;
margin-left: auto;
padding-left: 12px;
}
#playlist-submenu {
z-index: 210;
}
`;
`];
override connectedCallback() {
super.connectedCallback();
@@ -996,9 +947,6 @@ export class TrackList extends LitElement implements SelectionHost {
() => this.loadTracks(),
);
}
document.addEventListener('click', this.closeHandler);
document.addEventListener('contextmenu', this.closeHandler);
document.addEventListener('mousedown', this.mousedownCloseHandler);
document.addEventListener('mousedown', this.sortDropdownCloseHandler);
document.addEventListener('click', this.clearSelectionHandler);
document.addEventListener('mousemove', this.onColResizeMove);
@@ -1021,9 +969,6 @@ export class TrackList extends LitElement implements SelectionHost {
this.hasRestoredScroll = false;
super.disconnectedCallback();
this.cancelScanComplete?.();
document.removeEventListener('click', this.closeHandler);
document.removeEventListener('contextmenu', this.closeHandler);
document.removeEventListener('mousedown', this.mousedownCloseHandler);
document.removeEventListener('mousedown', this.sortDropdownCloseHandler);
document.removeEventListener('click', this.clearSelectionHandler);
document.removeEventListener('mousemove', this.onColResizeMove);
@@ -1178,30 +1123,7 @@ export class TrackList extends LitElement implements SelectionHost {
e.stopPropagation();
this.selection.handleContextMenu(track.FilePath);
this.contextMenuOpen = true;
// Position the popup at the mouse cursor using a virtual anchor.
this.updateComplete.then(() => {
const popup = this.contextMenuPopup;
if (popup) {
(popup as any).anchor = {
getBoundingClientRect() {
return {
width: 0,
height: 0,
x: e.clientX,
y: e.clientY,
top: e.clientY,
left: e.clientX,
right: e.clientX,
bottom: e.clientY,
};
},
};
(popup as any).active = true;
}
});
this.ctxMenu.openAt(e.clientX, e.clientY);
}
// =================================================================
@@ -1278,7 +1200,8 @@ export class TrackList extends LitElement implements SelectionHost {
break;
}
this.closeContextMenu(true);
this.selection.clear();
this.ctxMenu.close();
}
private openTrackDetails(filePath: string) {
@@ -1320,80 +1243,6 @@ export class TrackList extends LitElement implements SelectionHost {
};
}
private closeContextMenu(clearSelection = false) {
if (!this.contextMenuOpen) return;
this.closePlaylistSubmenu();
this.contextMenuOpen = false;
if (clearSelection) {
this.selection.clear();
}
const popup = this.contextMenuPopup;
if (popup) {
(popup as any).active = false;
}
}
private clearSubmenuCloseTimer() {
if (this.submenuCloseTimer !== null) {
clearTimeout(this.submenuCloseTimer);
this.submenuCloseTimer = null;
}
}
private scheduleSubmenuClose = () => {
this.clearSubmenuCloseTimer();
this.submenuCloseTimer = setTimeout(() => {
this.submenuCloseTimer = null;
this.closePlaylistSubmenu();
}, 150);
};
private async showPlaylistSubmenu() {
this.clearSubmenuCloseTimer();
if (this.playlistSubmenuOpen) return;
this.playlistSubmenuOpen = true;
await this.updateComplete;
const submenu = this.playlistSubmenuPopup;
const trigger = this.shadowRoot?.querySelector('.submenu-item');
if (submenu && trigger) {
(submenu as any).anchor = trigger;
(submenu as any).active = true;
}
const picker = this.shadowRoot?.querySelector(
'playlist-picker',
) as PlaylistPicker | null;
picker?.reset();
}
private closePlaylistSubmenu() {
this.clearSubmenuCloseTimer();
if (!this.playlistSubmenuOpen) return;
this.playlistSubmenuOpen = false;
const submenu = this.playlistSubmenuPopup;
if (submenu) {
(submenu as any).active = false;
}
}
private onPlaylistActionComplete = () => {
this.closeContextMenu(true);
};
// =================================================================
// Sort controls
// =================================================================
@@ -1781,28 +1630,28 @@ export class TrackList extends LitElement implements SelectionHost {
placement="bottom-start"
flip
shift
.active=${this.contextMenuOpen}
.active=${this.ctxMenu.contextMenuOpen}
>
${this.contextMenuOpen
${this.ctxMenu.contextMenuOpen
? html`
<div class="context-menu-panel">
<wa-dropdown-item
@click=${() => this.onContextMenuAction('play')}
@mouseenter=${() => this.closePlaylistSubmenu()}
@mouseenter=${() => this.ctxMenu.closePlaylistSubmenu()}
>
<wa-icon slot="icon" name="play"></wa-icon>
Play
</wa-dropdown-item>
<wa-dropdown-item
@click=${() => this.onContextMenuAction('add-to-queue')}
@mouseenter=${() => this.closePlaylistSubmenu()}
@mouseenter=${() => this.ctxMenu.closePlaylistSubmenu()}
>
<wa-icon slot="icon" name="plus"></wa-icon>
Add to Queue
</wa-dropdown-item>
<wa-dropdown-item
@click=${() => this.onContextMenuAction('play-next')}
@mouseenter=${() => this.closePlaylistSubmenu()}
@mouseenter=${() => this.ctxMenu.closePlaylistSubmenu()}
>
<wa-icon slot="icon" name="forward-step"></wa-icon>
Play Next
@@ -1810,13 +1659,13 @@ export class TrackList extends LitElement implements SelectionHost {
<wa-dropdown-item
class="submenu-item"
@mouseenter=${() => {
this.clearSubmenuCloseTimer();
void this.showPlaylistSubmenu();
this.ctxMenu.clearSubmenuCloseTimer();
void this.ctxMenu.showPlaylistSubmenu(this.selection.getSelectedKeysOrdered());
}}
@mouseleave=${this.scheduleSubmenuClose}
@mouseleave=${this.ctxMenu.scheduleSubmenuClose}
@click=${(e: Event) => {
e.stopPropagation();
void this.showPlaylistSubmenu();
void this.ctxMenu.showPlaylistSubmenu(this.selection.getSelectedKeysOrdered());
}}
>
<wa-icon slot="icon" name="plus"></wa-icon>
@@ -1830,8 +1679,8 @@ export class TrackList extends LitElement implements SelectionHost {
this.onContextMenuAction(
'track-details',
)}
@mouseenter=${() =>
this.closePlaylistSubmenu()}
@mouseenter=${() =>
this.ctxMenu.closePlaylistSubmenu()}
>
<wa-icon
slot="icon"
@@ -1851,18 +1700,18 @@ export class TrackList extends LitElement implements SelectionHost {
placement="right-start"
flip
shift
.active=${this.playlistSubmenuOpen}
.active=${this.ctxMenu.playlistSubmenuOpen}
>
${this.playlistSubmenuOpen && this.selection.hasSelection
${this.ctxMenu.playlistSubmenuOpen && this.selection.hasSelection
? html`
<div
@mouseenter=${() =>
this.clearSubmenuCloseTimer()}
@mouseleave=${this.scheduleSubmenuClose}
this.ctxMenu.clearSubmenuCloseTimer()}
@mouseleave=${this.ctxMenu.scheduleSubmenuClose}
>
<playlist-picker
.filePaths=${this.selection.getSelectedKeysOrdered()}
@playlist-action-complete=${this.onPlaylistActionComplete}
.filePaths=${this.ctxMenu.playlistFilePaths}
@playlist-action-complete=${this.ctxMenu.onPlaylistActionComplete}
@click=${(e: Event) => e.stopPropagation()}
></playlist-picker>
</div>