feat(frontend): render a multi-artist credit as one link per artist
Every artist name in the app went through `artistLink(name, mbid)`, so a track credited to several artists rendered one link and the rest as punctuation — "2Pac feat. Snoop Dogg" linked 2Pac and left Snoop Dogg as text inside it. `creditLink(parts, fallbackName, fallbackMbid)` renders the credit from its parts: one link per credited artist, join phrases as plain text between them. The link boundaries are known by construction, which is the point — locating a name inside the stored credit string would reintroduce the mismatch the catalog exists to avoid, since that string may come from the file's tags while the parts come from MusicBrainz and the two disagree for ~1 in 3 multi-artist credits. Fewer than two parts falls through to the previous behaviour exactly, so a single-artist credit, a file with no recording MBID and a catalog that has not answered yet all render as they did before. Nothing tries to split the fallback string: "Simon & Garfunkel" is one artist, which is why primaryArtist() does not split on "&" either. The lookup is keyed on the recording MBID, which both sides already carry — a catalog row has one and so does a local file — so one binding serves Explore and the library's own lists, and no local table is needed for this. credit-store.ts, and three things in it are load-bearing: - A miss is cached as an empty array. The backend returns nothing for a single-artist credit, which is ~87% of tracks, and caching only the hits would re-request the rest on every render forever. - request() is per-row and coalesces into one call per frame. A virtualized list cannot hand over "the whole list": 50,000 rows would be 100 queries for the ~30 on screen. - It is an LRU with a counted retainedChars probe, because a cache that grows with use is a leak with a schedule. The virtualized lists push requestUpdate() into the virtualizer rather than only the host, since its rows come from its own properties — a host update alone would leave them exactly as they were. now-playing marks its geometry dirty instead, because the marquee measures the text it is about to scroll. track-list keeps the single link while a search term is active: the highlight spans are computed against the flat credit string, and mapping them onto decomposed parts is a different problem. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AfVYUVExXsx1nSWrXN8mAh
This commit is contained in:
@@ -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 ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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`<div class="album-artist">
|
||||
${artistLink(artist, artistMbid)}
|
||||
${creditLink(
|
||||
creditStore.credits(this.releaseGroupMBID),
|
||||
artist,
|
||||
artistMbid,
|
||||
)}
|
||||
</div>`
|
||||
: nothing}
|
||||
${metaParts.length > 0
|
||||
|
||||
@@ -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
|
||||
<div class="album-title" title="${rg.title}">
|
||||
${rg.title}
|
||||
</div>
|
||||
<div class="album-artist">${artistLink(rg.artistCredit, rg.artistMbid ?? '')}</div>
|
||||
<div class="album-artist">${creditLink(creditStore.credits(rg.mbid), rg.artistCredit, rg.artistMbid ?? '')}</div>
|
||||
<div class="album-meta">
|
||||
<div class="album-meta-text">
|
||||
${rg.primaryType
|
||||
@@ -2255,7 +2264,7 @@ export class ExploreView extends ViewLifecycleMixin(LitElement) implements Conte
|
||||
${trackLink(r.title, r.releaseName ?? '', r.releaseGroupMbid ?? '', r.mbid)}
|
||||
</div>
|
||||
<div class="track-artist">
|
||||
${artistLink(r.artistCredit, r.artistMbid ?? '')}
|
||||
${creditLink(creditStore.credits(r.mbid), r.artistCredit, r.artistMbid ?? '')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="track-meta">
|
||||
|
||||
@@ -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')}
|
||||
>
|
||||
<span class="scroll-content">${artistLink(track.artist, track.artistMbid) || 'Unknown Artist'}</span>
|
||||
<span class="scroll-content">${creditLink(creditStore.credits(track.recordingMbid), track.artist, track.artistMbid) || 'Unknown Artist'}</span>
|
||||
</span>
|
||||
${describeQueueSource(this.queue.source)
|
||||
? html`
|
||||
|
||||
@@ -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}
|
||||
</div>
|
||||
<span class="cell col-title" title="${track.Title || track.FilePath}">${trackLink(track.Title, track.Album, track.ReleaseGroupMBID, track.RecordingMBID, undefined, track.Artist) || track.FilePath}</span>
|
||||
<span class="cell col-artist" title="${track.Artist}">${artistLink(track.Artist, track.ArtistMBID)}</span>
|
||||
<span class="cell col-artist" title="${creditText(creditStore.credits(track.RecordingMBID), track.Artist)}">${creditLink(creditStore.credits(track.RecordingMBID), track.Artist, track.ArtistMBID)}</span>
|
||||
<span class="cell col-album" title="${track.Album}">${albumLink(track.Album, track.ReleaseGroupMBID, undefined, track.Artist)}</span>
|
||||
<span class="cell col-duration">${formatMilliseconds(track.Duration)}</span>`}
|
||||
</div>
|
||||
|
||||
@@ -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)}
|
||||
</span>
|
||||
<span class="track-artist" title=${artist}>
|
||||
${artistLink(track.artist, track.artistMbid) || 'Unknown Artist'}
|
||||
${creditLink(creditStore.credits(track.recordingMbid), track.artist, track.artistMbid) || 'Unknown Artist'}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
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';
|
||||
@@ -51,7 +52,8 @@ import type { CoverArtUrls } from '@components/track-details/track-details.js';
|
||||
import { libraryStore } from '@store/library-store';
|
||||
import { formatMilliseconds } from '@utils/time';
|
||||
import {
|
||||
artistLink,
|
||||
creditLink,
|
||||
creditText,
|
||||
albumLink,
|
||||
trackLink,
|
||||
exploreLinkStyles,
|
||||
@@ -136,6 +138,9 @@ export class SmartPlaylistDetails
|
||||
* 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;
|
||||
|
||||
@@ -609,6 +614,14 @@ export class SmartPlaylistDetails
|
||||
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();
|
||||
});
|
||||
|
||||
if (this.autoEdit) {
|
||||
// Skip evaluation for new playlists — go straight to editor.
|
||||
this.autoEdit = false;
|
||||
@@ -649,6 +662,8 @@ export class SmartPlaylistDetails
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this.creditsUnsub?.();
|
||||
this.creditsUnsub = undefined;
|
||||
|
||||
if (this.playlistDeletedCleanup) {
|
||||
this.playlistDeletedCleanup();
|
||||
@@ -1423,7 +1438,7 @@ export class SmartPlaylistDetails
|
||||
: nothing}
|
||||
</div>
|
||||
<span class="cell col-title" title="${track.Title || track.FilePath}">${trackLink(track.Title, track.Album, track.ReleaseGroupMBID, track.RecordingMBID, undefined, track.Artist) || track.FilePath}</span>
|
||||
<span class="cell col-artist" title="${track.Artist}">${artistLink(track.Artist, track.ArtistMBID)}</span>
|
||||
<span class="cell col-artist" title="${creditText(creditStore.credits(track.RecordingMBID), track.Artist)}">${creditLink(creditStore.credits(track.RecordingMBID), track.Artist, track.ArtistMBID)}</span>
|
||||
<span class="cell col-album" title="${track.Album}">${albumLink(track.Album, track.ReleaseGroupMBID, undefined, track.Artist)}</span>
|
||||
<span class="cell col-duration">${formatMilliseconds(track.Duration)}</span>`}
|
||||
</div>
|
||||
|
||||
@@ -9,7 +9,8 @@ import {
|
||||
} from '@go/explore/service.js';
|
||||
import '../library-status-indicator/library-status-indicator.js';
|
||||
import type { LibraryStatus } from '../library-status-indicator/library-status-indicator.js';
|
||||
import { artistLink, exploreLinkStyles } from '../../utils/explore-link';
|
||||
import { creditLink, exploreLinkStyles } from '../../utils/explore-link';
|
||||
import { creditStore } from '@store/credit-store';
|
||||
import { libraryStatusFor } from '../../utils/library-status';
|
||||
import { downloadStore } from '../../store/download-store';
|
||||
|
||||
@@ -61,14 +62,23 @@ export class TopResultsRow extends LitElement {
|
||||
* the property and never updates this element. One subscription for
|
||||
* the row, not one per card.
|
||||
*/
|
||||
/** Unsubscribes the credit-arrival repaint. */
|
||||
private creditsUnsub?: () => void;
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
|
||||
this.creditsUnsub = creditStore.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
});
|
||||
this.unsubRequests = downloadStore.subscribe(() =>
|
||||
this.requestUpdate(),
|
||||
);
|
||||
}
|
||||
|
||||
override disconnectedCallback(): void {
|
||||
this.creditsUnsub?.();
|
||||
this.creditsUnsub = undefined;
|
||||
this.unsubRequests?.();
|
||||
this.unsubRequests = undefined;
|
||||
super.disconnectedCallback();
|
||||
@@ -327,7 +337,7 @@ export class TopResultsRow extends LitElement {
|
||||
${artistPart || metaPart
|
||||
? html`<span class="card-subtitle"
|
||||
>${artistPart
|
||||
? artistLink(artistPart, r.artistMbid ?? '')
|
||||
? creditLink(creditStore.credits(r.mbid), artistPart, r.artistMbid ?? '')
|
||||
: nothing}${artistPart && metaPart
|
||||
? ' · '
|
||||
: ''}${metaPart}</span
|
||||
|
||||
@@ -25,6 +25,7 @@ import type { SortOption } from '@components/page-header/page-header';
|
||||
import { TrackListController } from '@store/controllers/tracklist-controller';
|
||||
import { FavoritesController } from '@store/controllers/favorites-controller';
|
||||
import { queueStore } from '@store/queue-store';
|
||||
import { creditStore } from '@store/credit-store';
|
||||
import type { QueueSource } from '@store/queue-store';
|
||||
import { LibraryController } from '@store/controllers/library-controller';
|
||||
import {
|
||||
@@ -39,6 +40,7 @@ import {
|
||||
} from './search-ranking';
|
||||
import {
|
||||
artistLink,
|
||||
creditLink,
|
||||
albumLink,
|
||||
trackLink,
|
||||
exploreLinkStyles,
|
||||
@@ -1220,6 +1222,17 @@ export class TrackList
|
||||
'shortcut:tracklist-delete',
|
||||
this.handleShortcutDelete,
|
||||
);
|
||||
|
||||
// Credits arrive after the rows that asked for them. The
|
||||
// virtualizer produces its rows from its *own* properties, so a
|
||||
// host re-render alone repaints nothing — the same reason a
|
||||
// selection change pushes requestUpdate() into it.
|
||||
this.whileActive(
|
||||
creditStore.subscribe(() => {
|
||||
this.requestUpdate();
|
||||
this.virtualizer?.requestUpdate();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2031,7 +2044,15 @@ export class TrackList
|
||||
if (col.id === 'trackName') {
|
||||
display = trackLink(track.TrackName, track.Album, track.ReleaseGroupMBID, track.RecordingMBID, display as any, track.ArtistName);
|
||||
} else if (col.id === 'artistName') {
|
||||
display = artistLink(track.ArtistName, track.ArtistMBID, display as any);
|
||||
// A search term highlights the *flat* credit string,
|
||||
// and mapping those spans onto decomposed parts is a
|
||||
// different problem from rendering the credit. While
|
||||
// filtering, the single link is the honest answer.
|
||||
creditStore.request(track.RecordingMBID);
|
||||
const parts = term ? undefined : creditStore.get(track.RecordingMBID);
|
||||
display = parts && parts.length > 1
|
||||
? creditLink(parts, track.ArtistName, track.ArtistMBID)
|
||||
: artistLink(track.ArtistName, track.ArtistMBID, display as any);
|
||||
} else if (col.id === 'album') {
|
||||
display = albumLink(track.Album, track.ReleaseGroupMBID, display as any, track.ArtistName);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user