previews: the album button opens the record the previewed track came from

The third action in the preview modal's control row has been a disabled
stub since the modal landed -- there was nowhere for it to go until the
album page existed. It goes there now.

The target is the item's own provider `albumId`, never a title match, so a
top-tracks preview lands on the exact record the modal's metadata line is
already naming: Elephant Gym's "Light" opens Angle, and AC/DC's "Highway
to Hell" the album rather than any of the compilations carrying it. The
manifest has carried `albumId` and an `actions.openAlbum` flag from the
start; all 20 rows of a Deezer top-track payload declare one.

The navigation lands on the previewed TRACK, not just the record -- the
album page's existing pending-track flash does the scrolling and the
highlight, the same landing the now-playing song link already gets.

Three details that are not obvious:

- The overlay must not import state.svelte.js (the module comment on
  previewSession explains the cycle), so the navigator is injected by the
  page exactly as `playFull` already is.
- Both ids are read out of previewUi and the snapshot BEFORE the close, not
  after: closePreview() nulls the callback and drops the snapshot the
  active item came from. And the close happens first, because the session
  holds the audio focus and navigating under an open dialog would land the
  new page behind the scrim.
- contracts.js freezes every manifest id as a String, while `catalog_album`
  takes a u64 -- a JSON string fails to deserialize -- and the album page's
  flash compares track ids with ===. Both cross back to numbers in
  openAlbumFromPreviewItem, once, rather than at each call site.

An album session is already standing on the record it would open, so the
button greys out there with "You're already on this album" rather than
re-navigating to the page underneath. The decision is made from the
manifest's own contextKey, so it holds for any future surface that opens an
album-kind session. The album page still supplies the navigator, so a row
that ever declares a different album goes somewhere instead of being
silently dead.

Verified in the running app over CDP: artist top-tracks preview -> click ->
lands on Highway to Hell (id 9410086), pendingTrackId 92719900 as a Number,
matching track found on the page, .trow.flash present, modal gone, and the
glass bar trail reading Search -> AC/DC -> Highway to Hell. From an album
session the button is disabled and a click changes nothing. 85 JS tests
pass.
This commit is contained in:
KeelrLeek committed 2026-07-28 17:13:54 -05:00
1 parent 122157e061
commit 98d11a2228
5 files changed
+80 -2

No files matched your search

+5 -1
View File
@@ -24,6 +24,8 @@ export const previewUi = $state({
returnFocusTo: null,
/** page-supplied "play this item as a full track" starter */
playFull: null,
/** page-supplied "take me to this item's album" navigator */
openAlbum: null,
});
let service = null;
@@ -71,11 +73,12 @@ function attach(svc) {
/** Open a preview session over the production stack. `manifest` comes from
* a page adapter (artist page in Phase 3). */
export async function openPreview(manifest, { playFull } = {}) {
export async function openPreview(manifest, { playFull, openAlbum } = {}) {
if (!focusParts) throw new Error("preview bridge not initialised");
await closePreview(); // one session at a time, always torn down first
previewUi.returnFocusTo = document.activeElement;
previewUi.playFull = playFull ?? null;
previewUi.openAlbum = openAlbum ?? null;
attach(
createPreviewService({
resolver: createProductionResolver(),
@@ -171,6 +174,7 @@ export async function closePreview() {
previewUi.snapshot = null;
previewUi.focusWaiting = false;
previewUi.playFull = null;
previewUi.openAlbum = null;
return out;
}
+23
View File
@@ -2131,6 +2131,29 @@ export async function openCatalogAlbum(albumId, opts = {}) {
}
}
/**
* Open the album a preview-modal item declares, landing on the track that was
* being previewed.
*
* The record comes from the item's own provider `albumId` — never a title
* match — so a top-tracks preview lands on the single or compilation the
* modal's metadata line was already naming, not on a same-titled record.
*
* The conversion is the reason this is one function rather than a lambda at
* each call site: `contracts.js` freezes every manifest id as a STRING, while
* `catalog_album` takes a `u64` (a JSON string fails to deserialize) and the
* album page's flash compares track ids with `===`. Both cross back to numbers
* here, once.
*/
export function openAlbumFromPreviewItem(item) {
const albumId = Number(item?.albumId);
if (!Number.isFinite(albumId)) return;
const trackId = Number(item?.sources?.[0]?.providerTrackId);
return openCatalogAlbum(albumId, {
trackId: Number.isFinite(trackId) ? trackId : null,
});
}
const catNorm = (s) => (s || "").toLowerCase().replace(/[^a-z0-9]/g, "");
/** Resolve an artist by name (cached) and open their catalog page — used by