diff --git a/frontend/src/components/cover-grid/cover-grid.ts b/frontend/src/components/cover-grid/cover-grid.ts index 32fe657..6b3056d 100644 --- a/frontend/src/components/cover-grid/cover-grid.ts +++ b/frontend/src/components/cover-grid/cover-grid.ts @@ -52,7 +52,8 @@ import { } from '@utils/context-menu-controller.js'; import type { ContextMenuHost } from '@utils/context-menu-controller.js'; import { FavoritesController } from '@store/controllers/favorites-controller'; -import { artistLink, exploreLinkStyles } from '../../utils/explore-link'; +import { creditLink, exploreLinkStyles } from '../../utils/explore-link'; +import { creditStore } from '@store/credit-store'; import { createAlbumArtDragImage, createDragImage, @@ -425,8 +426,18 @@ export class CoverGrid * Lifecycle * ==================================================================== */ + /** Unsubscribes the credit-arrival repaint. */ + private creditsUnsub?: () => void; + override connectedCallback() { super.connectedCallback(); + + this.creditsUnsub = creditStore.subscribe(() => { + this.requestUpdate(); + // Two virtualizers when the grid is split; both draw rows. + this.renderRoot?.querySelectorAll('lit-virtualizer') + .forEach((v) => (v as unknown as { requestUpdate(): void }).requestUpdate()); + }); this.restoreSortPreferences(); this.loadAlbums(); @@ -441,6 +452,8 @@ export class CoverGrid override disconnectedCallback() { super.disconnectedCallback(); + this.creditsUnsub?.(); + this.creditsUnsub = undefined; this.removeEventListener( 'error', @@ -1820,7 +1833,7 @@ export class CoverGrid class="artist-name" title="${album.ArtistName}" > - ${artistLink(album.ArtistName, album.ArtistMBID ?? '')} + ${creditLink(creditStore.credits(album.MBID), album.ArtistName, album.ArtistMBID ?? '')} diff --git a/frontend/src/components/explore-album-details/explore-album-details.ts b/frontend/src/components/explore-album-details/explore-album-details.ts index 6682375..41abd21 100644 --- a/frontend/src/components/explore-album-details/explore-album-details.ts +++ b/frontend/src/components/explore-album-details/explore-album-details.ts @@ -20,7 +20,8 @@ type MBRelease = explore.MBRelease; type MBTrack = explore.MBTrack; import { exploreCache } from '../../store/explore-cache'; import { libraryStore } from '../../store/library-store'; -import { artistLink, exploreLinkStyles } from '../../utils/explore-link'; +import { creditLink, exploreLinkStyles } from '../../utils/explore-link'; +import { creditStore } from '@store/credit-store'; import { describeError } from '../../utils/describe-error'; import { EventsOn } from '@runtime/runtime'; import { Events } from '../../events'; @@ -704,8 +705,15 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { * BrowseReleases fetch never signals readiness. */ private releasesFallbackTimer?: number; + /** Unsubscribes the credit-arrival repaint. */ + private creditsUnsub?: () => void; + override connectedCallback() { super.connectedCallback(); + + this.creditsUnsub = creditStore.subscribe(() => { + this.requestUpdate(); + }); if (this.releaseGroupMBID || this.localAlbumId) { void this.loadAllData(); } @@ -760,6 +768,8 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { override disconnectedCallback() { super.disconnectedCallback(); + this.creditsUnsub?.(); + this.creditsUnsub = undefined; this.downloadUnsub?.(); this.downloadUnsub = null; this.unsubReleasesReady?.(); @@ -2850,7 +2860,11 @@ export class ExploreAlbumDetails extends LitElement implements ContextMenuHost { return html` ${artist ? html`
- ${artistLink(artist, artistMbid)} + ${creditLink( + creditStore.credits(this.releaseGroupMBID), + artist, + artistMbid, + )}
` : nothing} ${metaParts.length > 0 diff --git a/frontend/src/components/explore-view/explore-view.ts b/frontend/src/components/explore-view/explore-view.ts index fc4e47a..be05df2 100644 --- a/frontend/src/components/explore-view/explore-view.ts +++ b/frontend/src/components/explore-view/explore-view.ts @@ -16,7 +16,8 @@ import { exploreCache, ARTIST_IMAGE_CACHE_LIMIT } from '../../store/explore-cach import { queueStore } from '../../store/queue-store'; import { notificationStore } from '../../store/notification-store'; import '../notifications/inline-notice'; -import { artistLink, trackLink, exploreLinkStyles } from '../../utils/explore-link'; +import { creditLink, trackLink, exploreLinkStyles } from '../../utils/explore-link'; +import { creditStore } from '@store/credit-store'; import { describeError } from '../../utils/describe-error'; import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '../library-status-indicator/library-status-indicator.js'; @@ -774,6 +775,14 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte } protected override onViewActivate(): void { + // A cached primary view, so this is torn down on the way out + // rather than on disconnect — which never fires here. + this.whileActive( + creditStore.subscribe(() => { + this.requestUpdate(); + }), + ); + // Fetched on arrival rather than on connect: this is a cached // primary view, created and warmed at startup, so a fetch there // is three catalog queries every user pays for whether or not @@ -2193,7 +2202,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
${rg.title}
-
${artistLink(rg.artistCredit, rg.artistMbid ?? '')}
+
${creditLink(creditStore.credits(rg.mbid), rg.artistCredit, rg.artistMbid ?? '')}
${rg.primaryType @@ -2255,7 +2264,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte ${trackLink(r.title, r.releaseName ?? '', r.releaseGroupMbid ?? '', r.mbid)}
- ${artistLink(r.artistCredit, r.artistMbid ?? '')} + ${creditLink(creditStore.credits(r.mbid), r.artistCredit, r.artistMbid ?? '')}
diff --git a/frontend/src/components/now-playing/now-playing.ts b/frontend/src/components/now-playing/now-playing.ts index 8722625..6d12938 100644 --- a/frontend/src/components/now-playing/now-playing.ts +++ b/frontend/src/components/now-playing/now-playing.ts @@ -4,7 +4,7 @@ import '@awesome.me/webawesome/dist/components/icon/icon.js'; import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import { - artistLink, + creditLink, trackLink, exploreLinkStyles, } from '@utils/explore-link'; @@ -14,6 +14,7 @@ import { navigateToQueueSource, } from '@utils/queue-source-link'; import { PlayerController } from '@store/controllers/player-controller'; +import { creditStore } from '@store/credit-store'; import { QueueController } from '@store/controllers/queue-controller'; import { FavoritesController } from '@store/controllers/favorites-controller'; import { designTokens } from '../../styles/tokens.css'; @@ -290,6 +291,9 @@ export class NowPlaying extends LitElement { } `]; + /** Unsubscribes the credit-arrival repaint. */ + private creditsUnsub?: () => void; + override connectedCallback() { super.connectedCallback(); this.loadScrollMode(); @@ -306,10 +310,21 @@ export class NowPlaying extends LitElement { this.geometryDirty = true; this.requestUpdate(); }); + + // A credit arriving changes the rendered text, and the marquee + // measures that text — so this is a geometry change, not just a + // repaint. Saying so is what stops the bar scrolling to the + // old width. + this.creditsUnsub = creditStore.subscribe(() => { + this.geometryDirty = true; + this.requestUpdate(); + }); } override disconnectedCallback() { super.disconnectedCallback(); + this.creditsUnsub?.(); + this.creditsUnsub = undefined; // A drag interrupted by the bar going away still has to clean up. this.attachDragListeners(false); window.removeEventListener(SCROLL_CHANGE_EVENT, this.handleScrollModeEvent); @@ -441,7 +456,7 @@ export class NowPlaying extends LitElement { @mouseleave=${this.handleArtistMouseLeave} @transitionend=${() => this.onScrollCycleEnd('artist')} > - ${artistLink(track.artist, track.artistMbid) || 'Unknown Artist'} + ${creditLink(creditStore.credits(track.recordingMbid), track.artist, track.artistMbid) || 'Unknown Artist'} ${describeQueueSource(this.queue.source) ? html` diff --git a/frontend/src/components/playlist-details/playlist-details.ts b/frontend/src/components/playlist-details/playlist-details.ts index 1c646aa..e188099 100644 --- a/frontend/src/components/playlist-details/playlist-details.ts +++ b/frontend/src/components/playlist-details/playlist-details.ts @@ -24,6 +24,7 @@ import type * as playlist from '@go/playlist/models.js'; import { EventsOn } from '@runtime/runtime'; import { Events } from '../../events'; import { queueStore } from '@store/queue-store'; +import { creditStore } from '@store/credit-store'; import { PlayerController } from '@store/controllers/player-controller'; import { SearchController } from '@store/controllers/search-controller'; import { SelectionController } from '@utils/selection-controller'; @@ -61,7 +62,8 @@ import '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js'; import type { DuplicateTracksDialog } from '@components/duplicate-tracks-dialog/duplicate-tracks-dialog.js'; import { formatMilliseconds } from '@utils/time'; import { - artistLink, + creditLink, + creditText, albumLink, trackLink, exploreLinkStyles, @@ -106,6 +108,9 @@ export class PlaylistDetails * rather than guessed. Without the hint the flow layout's 100 px * default drives constant scroll-error correction, which reads as * the list jumping under the pointer. */ + /** Unsubscribes the credit-arrival repaint. */ + private creditsUnsub?: () => void; + @query('lit-virtualizer') private virtualizer?: LitVirtualizer; @@ -224,6 +229,14 @@ export class PlaylistDetails override connectedCallback() { super.connectedCallback(); + + // Credits arrive after the rows that asked for them, and a + // virtualizer repaints from its *own* properties — a host + // update alone leaves the rows exactly as they were. + this.creditsUnsub = creditStore.subscribe(() => { + this.requestUpdate(); + this.virtualizer?.requestUpdate(); + }); this.loadTracks(); this.tracksChangedCleanup = EventsOn( @@ -248,6 +261,8 @@ export class PlaylistDetails override disconnectedCallback() { super.disconnectedCallback(); + this.creditsUnsub?.(); + this.creditsUnsub = undefined; if (this.tracksChangedCleanup) { this.tracksChangedCleanup(); @@ -1575,7 +1590,7 @@ export class PlaylistDetails : nothing}
${trackLink(track.Title, track.Album, track.ReleaseGroupMBID, track.RecordingMBID, undefined, track.Artist) || track.FilePath} - ${artistLink(track.Artist, track.ArtistMBID)} + ${creditLink(creditStore.credits(track.RecordingMBID), track.Artist, track.ArtistMBID)} ${albumLink(track.Album, track.ReleaseGroupMBID, undefined, track.Artist)} ${formatMilliseconds(track.Duration)}`} diff --git a/frontend/src/components/queue-panel/queue-panel.ts b/frontend/src/components/queue-panel/queue-panel.ts index ab6da06..2e67e2c 100644 --- a/frontend/src/components/queue-panel/queue-panel.ts +++ b/frontend/src/components/queue-panel/queue-panel.ts @@ -12,6 +12,7 @@ import '@awesome.me/webawesome/dist/components/popup/popup.js'; import type WaPopup from '@awesome.me/webawesome/dist/components/popup/popup.js'; import '@awesome.me/webawesome/dist/components/dropdown-item/dropdown-item.js'; import { QueueController } from '@store/controllers/queue-controller'; +import { creditStore } from '@store/credit-store'; import { describeQueueSource, isQueueSourceNavigable, @@ -55,7 +56,7 @@ import { tracksByFilePath } from '@utils/track-index.js'; import type { TrackDetails } from '@components/track-details/track-details.js'; import type { CoverArtUrls } from '@components/track-details/track-details.js'; import { - artistLink, + creditLink, trackLink, exploreLinkStyles, } from '@utils/explore-link'; @@ -108,6 +109,9 @@ export class QueuePanel @query('#playlist-submenu') private playlistSubmenuPopup!: WaPopup; + /** Unsubscribes the credit-arrival repaint. */ + private creditsUnsub?: () => void; + @query('lit-virtualizer') private virtualizer!: LitVirtualizer; @@ -635,6 +639,14 @@ export class QueuePanel override connectedCallback() { super.connectedCallback(); + + // Credits arrive after the rows that asked for them, and a + // virtualizer repaints from its *own* properties — a host + // update alone leaves the rows exactly as they were. + this.creditsUnsub = creditStore.subscribe(() => { + this.requestUpdate(); + this.virtualizer?.requestUpdate(); + }); this.style.setProperty( '--queue-width', `${this.panelWidth}px`, @@ -671,6 +683,8 @@ export class QueuePanel override disconnectedCallback() { super.disconnectedCallback(); + this.creditsUnsub?.(); + this.creditsUnsub = undefined; document.removeEventListener( 'mousemove', this.handleMouseMove, @@ -1675,7 +1689,7 @@ export class QueuePanel ${trackLink(title, track.album, track.releaseGroupMbid, track.recordingMbid, undefined, track.artist)} - ${artistLink(track.artist, track.artistMbid) || 'Unknown Artist'} + ${creditLink(creditStore.credits(track.recordingMbid), track.artist, track.artistMbid) || 'Unknown Artist'}