import { LitElement, html, css, nothing } from 'lit'; import { customElement, state, query } from 'lit/decorators.js'; import { EventsEmit } from '@runtime/runtime'; import { GetAllAlbums, GetAlbumTracks } from '@go/library/Library'; import { library } from '@go/models'; import { QueueController } from '@store/controllers/queue-controller'; import '@lit-labs/virtualizer'; import { grid } from '@lit-labs/virtualizer/layouts/grid.js'; 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'; @customElement('cover-grid') export class CoverGrid extends LitElement { private queue = new QueueController(this); private closeHandler = () => this.closeContextMenu(); static override styles = css` :host { display: flex; flex-direction: column; overflow: hidden; } lit-virtualizer { flex: 1; overflow-y: auto; } .album-card { display: flex; flex-direction: column; cursor: pointer; border-radius: 8px; padding: 8px; transition: background-color 0.2s ease; box-sizing: border-box; } .album-card:hover { background-color: rgba(255, 255, 255, 0.1); } .album-card:focus { outline: 2px solid #1db954; outline-offset: 2px; } .cover-container { position: relative; width: 100%; aspect-ratio: 1; border-radius: 4px; overflow: hidden; background-color: #282828; } .cover-image { width: 100%; height: 100%; object-fit: cover; } .placeholder-cover { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; background: linear-gradient(135deg, #404040 0%, #282828 100%); color: #b3b3b3; font-size: 48px; } .album-info { margin-top: 8px; min-width: 0; } .album-name { font-size: 14px; font-weight: 600; color: #fff; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .artist-name { font-size: 12px; color: #b3b3b3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 4px; } .loading { display: flex; justify-content: center; align-items: center; padding: 32px; color: #b3b3b3; } .empty-state { display: flex; flex-direction: column; justify-content: center; align-items: center; padding: 48px; color: #b3b3b3; text-align: center; } .empty-state p { margin: 8px 0; } #context-menu { z-index: 200; } .context-menu-panel { background-color: #2a2a3e; border: 1px solid #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::part(base) { color: #e0e0e0; font-size: 13px; } .context-menu-panel wa-dropdown-item::part(base):hover { background-color: rgba(255, 255, 255, 0.1); } `; @state() private albums: library.Album[] = []; @state() private loading = true; @state() private contextMenuOpen = false; @state() private contextMenuAlbum: library.Album | null = null; @query('#context-menu') private contextMenuPopup!: HTMLElement; override connectedCallback() { super.connectedCallback(); this.loadAlbums(); document.addEventListener('click', this.closeHandler); document.addEventListener('contextmenu', this.closeHandler); } override disconnectedCallback() { super.disconnectedCallback(); document.removeEventListener('click', this.closeHandler); document.removeEventListener('contextmenu', this.closeHandler); } private async loadAlbums() { try { this.loading = true; const albums = await GetAllAlbums(); this.albums = albums ?? []; } catch (error) { console.error("Error loading albums:", error); this.albums = []; } finally { this.loading = false; } } private async getAlbumFilePaths(album: library.Album): Promise { try { const tracks = await GetAlbumTracks(album.ID); return tracks.map((t) => t.FilePath); } catch (error) { console.error("Error loading album tracks:", error); return []; } } private onAlbumContextMenu(e: MouseEvent, album: library.Album) { e.preventDefault(); e.stopPropagation(); this.contextMenuAlbum = album; this.contextMenuOpen = true; 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; } }); } private async onContextMenuAction(action: string) { if (!this.contextMenuAlbum) return; const filePaths = await this.getAlbumFilePaths(this.contextMenuAlbum); if (filePaths.length === 0) return; switch (action) { case 'play': this.queue.setQueue(filePaths, 0); break; case 'add-to-queue': this.queue.addTracksToQueue(filePaths); break; case 'play-next': this.queue.playTracksNext(filePaths); break; } this.closeContextMenu(); } private closeContextMenu() { if (!this.contextMenuOpen) return; this.contextMenuOpen = false; this.contextMenuAlbum = null; const popup = this.contextMenuPopup; if (popup) { (popup as any).active = false; } } private renderAlbumCard = (album: library.Album): unknown => { return html`
this.onAlbumClick(album)} @keydown=${(e: KeyboardEvent) => this.onAlbumKeydown(e, album)} @contextmenu=${(e: MouseEvent) => this.onAlbumContextMenu(e, album)} >
${album.CoverArtPath ? html`${album.Name} cover { const img = e.target as HTMLImageElement; if (img.src !== album.CoverArtPath) { img.src = album.CoverArtPath; } }} />` : html`
${this.getAlbumInitial(album.Name)}
`}
${album.Name}
${album.ArtistName}${album.Year ? ` - ${album.Year}` : ''}
`; }; private getAlbumInitial(name: string): string { return name.charAt(0).toUpperCase(); } private onAlbumClick(album: library.Album) { EventsEmit('AlbumSelected', album); this.dispatchEvent( new CustomEvent('album-selected', { detail: album, bubbles: true, composed: true, }) ); } private onAlbumKeydown(e: KeyboardEvent, album: library.Album) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); this.onAlbumClick(album); } } override render() { if (this.loading) { return html`
Loading albums...
`; } if (this.albums.length === 0) { return html`

No albums found

Add music to your library to see album covers here.

`; } return html` ${this.contextMenuOpen ? html`
this.onContextMenuAction('play')} > Play this.onContextMenuAction('add-to-queue')} > Add to Queue this.onContextMenuAction('play-next')} > Play Next
` : nothing}
`; } } declare global { interface HTMLElementTagNameMap { 'cover-grid': CoverGrid; } }