search: the results page, the typeahead, and a session that survives a reload

The page is the signed-off prototype built out: floating rounded hero, inline
top-tracks panel, filter chips, two-column Songs|Albums beside Artists, and all
seven states — results, searching, no results, one-kind-only, library-empty,
provider-unreachable, first visit. MusicBrainz records are merged in silently
behind the Deezer rows.

The top result is ranked by POPULARITY, not by kind: each kind puts up its most
popular exact match and the winner is whichever holds the largest share of its
own kind's leader. A raw fans-vs-rank comparison was tried first and handed
"panda bear" to an Owl City song.

Two provenance traps are paid for here. Deezer's /search/album returns no
release_date at all, so sorting newest-first ordered by PROVENANCE rather than
by date; dates are backfilled from the artist overview, resolved PER ALBUM
ARTIST because Deezer's artist search answers nothing for "weezer blue album".
And the typeahead holds its keyboard selection as the row's KEY rather than its
index, because the MusicBrainz supplement lands ~600ms later and re-orders the
album group under the cursor.

The hero's middle third is hidden on the width of the PANE, not the window. The
sidebar and the now-playing rail take ~490px between them, so a 1504px window
can leave the hero 1012px — a window-width media query cannot see that, and the
"latest release" card ended up sheared down to 26px of itself. The budget is
written out at the @container rule.

Session restore rounds it off: reload or restart returns to the same place with
the same track, everything but the transport coming from localStorage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
KeelrLeekandClaude Opus 5 committed 2026-08-03 07:03:45 -05:00
1 parent 7547fed56d
commit b173f60085
6 files changed
+3315 -493

No files matched your search

+13
View File
@@ -77,6 +77,10 @@ export const playLocal = (path, duration) => invoke("play_local", { path, durati
export const preloadLocal = (path, duration) => invoke("preload_local", { path, duration });
export const playerControl = (action, value, rate) =>
invoke("player_control", { action, value: value ?? null, rate: rate ?? null });
// What the audio thread is doing right now. The webview only ever learns about
// playback from events, so after a reload this is the only way to find out that
// audio is still running — see `restoreSession`.
export const playerState = () => invoke("player_state");
export const waveform = (path, buckets) => invoke("waveform", { path, buckets });
export const libraryAlbums = () => invoke("library_albums");
export const albumTracks = (albumId) => invoke("album_tracks", { albumId });
@@ -149,6 +153,15 @@ export const catalogArtistPopular = (artistId) =>
export const pickArtistHero = (artistId) => invoke("pick_artist_hero", { artistId });
export const clearArtistHero = (artistId) => invoke("clear_artist_hero", { artistId });
export const catalogRelated = (artistId) => invoke("catalog_related", { artistId });
// Deferred Last.fm pass for search results: `tracks` is [{ artistId, title }]
// and the answer comes back in the same order. Kept OUT of catalog_search,
// which the typeahead calls per keystroke and which must never wait on
// MusicBrainz's 1 req/s gate.
export const catalogTrackListeners = (tracks) =>
invoke("catalog_track_listeners", { tracks });
// Genres + Last.fm totals for the search hero's artist. Deferred, like the rest.
export const catalogArtistProfile = (artistId) =>
invoke("catalog_artist_profile", { artistId });
export const catalogAlbum = (albumId) => checkedAlbumPayload(albumId);
export const catalogAlbumInfo = (artistId, artist, title) =>
invoke("catalog_album_info", { artistId: artistId ?? null, artist, title });