diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-16 16:07:06 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-16 16:07:06 +0200 |
| commit | 25f62e169e049f0757ef611d5b409e7523958dee (patch) | |
| tree | cb755a6ab7364b1f4bc2144caf04b7310862aa13 /packages/meshbay-hub/src/meshbay_hub/static | |
| parent | 8889e90b8f1629f983c7ed4e44a2badbda525095 (diff) | |
| download | meshbay-25f62e169e049f0757ef611d5b409e7523958dee.tar.gz | |
music: pool single-album artists into shared rows
An artist with one album got a heading and one cover on a row that fits
five, and a library is mostly single-album artists. Consecutive singles
share one grid, in place, so the page stays in artist order.
Each pooled cover keeps its artist's name above it in the same type as
a section heading. Dropping it was the first version and it was wrong:
scrolling then alternates between artists written large and small.
Measured on the probe's fixture: 4208px to 1895px, and a walk of the
page reaches all 21 covers instead of 9.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/static')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/music-app.js | 94 | ||||
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/static/style.css | 24 |
2 files changed, 100 insertions, 18 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js index 8fc6cb3..baa3520 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/music-app.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/music-app.js @@ -348,10 +348,29 @@ function MusicDetailModal({ album, transportRef, gekRef, musicbrainzEnabled, onC // -- Mode A: album grid ----------------------------------------------------- -// `units` is one page of `{ artist, album }`. An artist whose albums straddle -// two pages gets its heading on both. -function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueue, onMenu }) { - const [detail, setDetail] = useState(null); // the album object +/** + * One page of `{ artist, album }` as the sections that get drawn. + * + * An artist with two or more albums gets a heading and a grid of their own. An + * artist with **one** does not: a heading plus a single cover is a whole row of + * whitespace, and a library is mostly single-album artists — a compilation + * bought once, one album of someone's, a soundtrack. Consecutive singles share + * one grid instead, so five of them fill a row that five headings would + * otherwise have spent five rows on. + * + * Pooled in place rather than swept into a bin at the end: the page is drawn in + * artist order and a reader scrolling it is relying on that. A pooled run sits + * exactly where its artists would have been. + * + * **Each pooled cover keeps its artist's name above it, in the same type as a + * multi-album artist's heading.** The first version of this dropped the heading + * on the grounds that the card already names its artist underneath — and that + * was wrong: scrolling then alternates between artists written large and + * artists written small, and the eye has to work out which kind of row it is + * looking at. The heading moves *into* the cell rather than going away, so a + * row of five costs one heading's height between them instead of five rows. + */ +function albumSections(units) { const artists = []; for (const u of units) { const prev = artists[artists.length - 1]; @@ -359,21 +378,60 @@ function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueu else artists.push({ artist: u.artist, albums: [u.album] }); } + const sections = []; + for (const a of artists) { + if (a.albums.length >= 2) { sections.push({ kind: 'artist', ...a }); continue; } + const prev = sections[sections.length - 1]; + if (prev && prev.kind === 'pool') prev.albums.push(a.albums[0]); + else sections.push({ kind: 'pool', albums: [a.albums[0]] }); + } + return sections; +} + +// `units` is one page of `{ artist, album }`. An artist whose albums straddle +// two pages gets its heading on both — and, with one album on each page, is +// pooled on both. +function AlbumGrid({ units, transportRef, gekRef, musicbrainzEnabled, onPlayQueue, onMenu }) { + const [detail, setDetail] = useState(null); // the album object + const sections = useMemo(() => albumSections(units), [units]); + + const tile = (album) => html` + <${LazyTile} key=${album.artist + '::' + album.album} cls="music-tile-slot"> + <${AlbumCard} album=${album} transportRef=${transportRef} gekRef=${gekRef} + musicbrainzEnabled=${musicbrainzEnabled} onOpen=${() => setDetail(album)} + onMenu=${onMenu} /> + </${LazyTile}> + `; + + // Keyed on the first album rather than on the index: a pool's position shifts + // whenever a neighbouring artist gains or loses an album, and an index key + // would make preact reuse the wrong tiles across that change. + const sectionKey = (s) => (s.kind === 'artist' + ? `artist:${s.artist}` + : `pool:${s.albums[0].artist}::${s.albums[0].album}`); + return html` - ${artists.map((a) => html` - <div class="music-artist-section" key=${a.artist}> - <h3 class="music-artist-heading">${a.artist}</h3> - <div class="music-grid"> - ${a.albums.map((album) => html` - <${LazyTile} key=${album.artist + '::' + album.album} cls="music-tile-slot"> - <${AlbumCard} album=${album} transportRef=${transportRef} gekRef=${gekRef} - musicbrainzEnabled=${musicbrainzEnabled} onOpen=${() => setDetail(album)} - onMenu=${onMenu} /> - </${LazyTile}> - `)} - </div> - </div> - `)} + ${sections.map((s) => (s.kind === 'artist' + ? html` + <div class="music-artist-section" key=${sectionKey(s)}> + <h3 class="music-artist-heading">${s.artist}</h3> + <div class="music-grid">${s.albums.map(tile)}</div> + </div>` + : html` + <div class="music-artist-section music-artist-pool" key=${sectionKey(s)}> + <div class="music-grid"> + ${s.albums.map((album) => html` + <div class="music-pool-cell" key=${album.artist + '::' + album.album}> + ${/* One line, clipped, with the full name on hover: a cell is + ~170px wide and a heading that wraps to two lines would + push its own cover below the others on the row. */''} + <h3 class="music-artist-heading music-pool-heading" + title=${album.artist}>${album.artist}</h3> + ${tile(album)} + </div> + `)} + </div> + </div>`))} ${detail && html` <${MusicDetailModal} album=${detail} transportRef=${transportRef} gekRef=${gekRef} musicbrainzEnabled=${musicbrainzEnabled} onMenu=${onMenu} diff --git a/packages/meshbay-hub/src/meshbay_hub/static/style.css b/packages/meshbay-hub/src/meshbay_hub/static/style.css index 2bf0b18..5ef04b0 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/style.css +++ b/packages/meshbay-hub/src/meshbay_hub/static/style.css @@ -5044,6 +5044,12 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } max-height: calc(100vh - 16px); overflow-x: hidden; overflow-y: auto; + /* Wheeling past the last track must not carry on into the page behind: that + scroll is a real page scroll, and a page scroll closes this menu on + purpose (menu.js). Without this, reaching the end of a long tracklist + dismisses the menu, which is the same symptom the scroll handler was just + fixed for, arriving by a different door. */ + overscroll-behavior: contain; padding: 4px 0; background: var(--bg-surface); border: 1px solid var(--border); @@ -5192,3 +5198,21 @@ h2 .gn-owner, h3 .gn-owner { font-size: 0.55em; } overflow: hidden; text-overflow: ellipsis; } + +/* A run of single-album artists, pooled into one grid so five of them fill one + row instead of spending five (music-app.js `albumSections`). + + Each cell keeps its artist's name above the cover, in the same type as a + multi-album artist's heading. Dropping it — the first version of this — made + scrolling alternate between artists written large and artists written small, + which is a worse problem than the whitespace it saved. */ +.music-artist-pool { margin-bottom: 22px; } +.music-pool-cell { display: flex; flex-direction: column; min-width: 0; } +/* One line, clipped. A cell is about 170px wide, and a heading that wrapped to + two lines would push its own cover below the others on its row — the grid + stretches the cell, it does not align what is inside it. */ +.music-pool-heading { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} |