import { LitElement, html, css } from 'lit'; import { customElement, property, state, } from 'lit/decorators.js'; import * as library from '@go/library/models.js'; import { LibraryController } from '@store/controllers/library-controller'; import { GetArtistImageURL, GetArtistImageCachedPath, GetArtistMBID, } from '@go/explore/service.js'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@components/cover-grid/cover-grid.js'; import { designTokens } from '../../styles/tokens.css'; import { backButton } from '../../styles/back-button.css'; @customElement('artist-details') export class ArtistDetails extends LitElement { @property({ type: Number, attribute: 'artist-id' }) artistId = 0; @property({ type: String, attribute: 'artist-name' }) artistName = ''; @property({ type: String, attribute: 'artist-mbid' }) artistMBID = ''; @state() private albums: library.Album[] = []; @state() private loading = true; @state() private artistImageURL = ''; private libraryCtrl = new LibraryController(this); /** Tracks the store's cached array reference to detect refreshes. */ private lastAlbumsRef: library.Album[] | null = null; static override styles = [designTokens, backButton, css` :host { display: flex; flex-direction: column; overflow: hidden; height: 100%; box-sizing: border-box; } /* ==================================== * Header * ==================================== */ .artist-header { display: flex; align-items: center; gap: 20px; padding: 16px 20px; flex-shrink: 0; border-bottom: 1px solid var( --yj-border-subtle, rgba(255, 255, 255, 0.06) ); } .back-button wa-icon { font-size: 16px; /* back button — outside type scale */ } .artist-avatar { width: 80px; height: 80px; border-radius: 50%; overflow: hidden; background: linear-gradient( 135deg, var(--yj-bg-overlay, #404040) 0%, var(--yj-bg-surface, #282828) 100% ); display: flex; align-items: center; justify-content: center; flex-shrink: 0; } .artist-avatar img { width: 100%; height: 100%; object-fit: cover; } .artist-avatar .initial { color: var( --yj-text-secondary, #b3b3b3 ); font-size: 32px; /* large decorative initial */ font-weight: 600; text-transform: uppercase; user-select: none; line-height: 1; } .artist-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; } .artist-title { font-size: 24px; /* page title — outside type scale */ font-weight: 700; color: var(--yj-text-primary, #fff); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin: 0; line-height: 1.2; } .album-count { font-size: var(--yj-text-md); color: var( --yj-text-secondary, #b3b3b3 ); } /* ==================================== * Content * ==================================== */ .content { flex: 1; overflow: hidden; } cover-grid { width: 100%; height: 100%; } `]; override connectedCallback() { super.connectedCallback(); this.loadAlbums(); this.loadArtistImage(); } override updated() { const cached = this.libraryCtrl.cachedAlbums; if ( cached !== null && cached !== this.lastAlbumsRef ) { this.lastAlbumsRef = cached; this.loadAlbums(); } } /* ================================================================ * Data loading * ================================================================ */ private async loadArtistImage() { // Resolve MBID from tags if not provided via attribute. let mbid = this.artistMBID; if (!mbid && this.artistName) { try { mbid = await GetArtistMBID(this.artistName); } catch { return; } } if (!mbid) return; try { // Disk cache first — the resolving call below is MB → // Wikidata → Wikipedia → Wikimedia, and most artists this // page renders have been resolved once already. const cached = await GetArtistImageCachedPath(mbid); if (cached) { this.artistImageURL = cached; return; } const url = await GetArtistImageURL(mbid); if (url) { this.artistImageURL = url; } } catch { // No image — avatar stays as initial letter. } } private async loadAlbums() { if (!this.artistId) return; // Try to populate instantly from the // cached all-albums list if available. const cached = this.libraryCtrl.getAlbumsByArtistNameCached( this.artistName, ); if (cached !== null && cached.length > 0) { this.albums = cached; this.loading = false; } // Always run the authoritative backend // query. If we got a cache hit above, // this serves as a correction pass. try { const albums = await this.libraryCtrl.getAlbumsByArtist( this.artistName, ); const result = albums ?? []; // Skip update if the cached result // is identical (same IDs in same // order) to avoid a re-render. if (!this.albumsMatch(result)) { this.albums = result; } } catch (error) { console.error( 'Error loading artist albums:', error, ); // Only overwrite if we had no cached // result to fall back on. if (cached === null) { this.albums = []; } } finally { this.loading = false; } } /** * Compare two album lists by ID to avoid * unnecessary re-renders when the backend * result matches the cached approximation. */ private albumsMatch( incoming: library.Album[], ): boolean { const current = this.albums; if (current.length !== incoming.length) { return false; } for (let i = 0; i < current.length; i++) { if ( current[i]!.ID !== incoming[i]!.ID ) { return false; } } return true; } /* ================================================================ * Navigation * ================================================================ */ private navigateBack() { this.dispatchEvent( new CustomEvent('navigate', { bubbles: true, composed: true, detail: { view: 'artists' }, }), ); } /* ================================================================ * Helpers * ================================================================ */ private getInitial(name: string): string { if (!name) return '?'; return name.charAt(0).toUpperCase(); } /* ================================================================ * Rendering * ================================================================ */ override render() { const albumCount = this.albums.length; const albumLabel = albumCount === 1 ? 'album' : 'albums'; return html`
${this.artistImageURL ? html`${this.artistName}` : html` ${this.getInitial(this.artistName)} `}

${this.artistName}

${!this.loading ? html` ${albumCount} ${albumLabel} ` : ''}
`; } }