watchparty/client/src/musicBar/MusicBar.css
Nicolas e89a81a3f1 Cast a listen party too, and fix a slow/freezing catalog
Three separate reports handled together.

Cast was video-only by design (docs/architecture.md flagged it as
video-parties-only for v1) - asked for directly, and turned out to need
almost nothing new server-side: CastTokenService and the 4 streaming
routes' sessionForStream fallback were already keyed on nothing more
specific than partyId. cast.ts's localDriver only needed its parameter
widened from HTMLVideoElement to HTMLMediaElement - both already share
the same currentTime/paused/duration/play()/pause() surface. MusicBar.ts
gained the same activeDriver indirection Player.ts already has, with one
real difference from the video case: a listen party's queue advances
far more often than a video party changes items, so dropping the cast
on every track (the video path's own choice there) would have made this
worse than not having it - a track change while casting instead calls
startCast() again, reusing the already-connected session (a
cache-busting query param added since the content URL itself never
changes between tracks - the server alone decides what's current).
Falls back to local playback if a re-cast throws, same recovery
instinct loadTrack's own hls.js try/catch already has.

The queue popup: reported separately, a fixed 480px width and a header
that scrolled away with the list - both leftover CSS from before the
row shape was reworked, never revisited since. Width cap dropped;
overflow/max-height moved off the outer .musicBarQueue (which wraps the
header too) down onto #queueList alone.

The catalog: still too slow, the front freezing while it loads.
CatalogService.getLibraryItems was asking Emby for Overview on every
one of up to 300 items and never using it (views/library's tiles only
ever show a poster/name/year) - dropped. getArtists had no Limit at
all, unlike every sibling query. Separately, tiles.ts's shared render
path built one HTML string for a whole batch and inserted it via a
single innerHTML write - fine for a 16-item carousel, a real hitch for
a 300-tile grid. renderTilesInChunks inserts the same tileHtml output
40 at a time across requestAnimationFrame callbacks instead;
wireTileButtons switched to one delegated listener so a tile from a
later chunk is still clickable.

Verified: full typecheck/test/build on both projects (122/122 server
tests). The chunked-render mechanism was verified directly - a
synthetic 300-item list against a real DOM confirms every item lands
and a button in the last chunk still fires through the delegated
listener. Not verified: no real Chromecast or Emby login reachable from
this sandbox, so the actual on-TV re-cast round trip and the
before/after catalog latency against a real large library are both
unconfirmed - same standing gap this project already flags for its
other casting work.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-15 21:32:34 +02:00

176 lines
5.1 KiB
CSS

/* Persistent bottom overlay for a music listen party - see MusicBar.ts's
header comment for why this is a Shell-level component instead of a
route (views/player/Player.css's .playerWrap is the video equivalent,
and stays a full route takeover on purpose - a shared audio queue has
no image to "theater mode" around, and the whole point here is staying
able to browse/add tracks while it plays). Sits above .mainContent, at
a lower z-index than .playerWrap (20) so a video party's takeover still
correctly covers it - moot in practice since joining a video party
calls leaveMusicParty() first (see Player.ts), but cheap to get right. */
.musicBar {
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 15;
background: var(--bg-header);
border-top: 1px solid rgba(255, 255, 255, .08);
box-shadow: 0 -4px 16px rgba(0, 0, 0, .3);
}
/* A thin strip along the bar's own top edge, not inside .musicBarRow -
the same "compact player" convention Spotify/YouTube Music use for
their mini-players. Read-only (see MusicBar.ts's own comment) - no
click/drag handling, just a visual position indicator. */
.musicBarProgressTrack {
height: 3px;
background: rgba(255, 255, 255, .12);
}
.musicBarProgressFill {
height: 100%;
width: 0%;
background: var(--accent);
transition: width .25s linear;
}
.musicBarRow {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 16px;
flex-wrap: wrap;
}
/* Smaller than the video player's presence row (Player.css/presence.css's
36px default) - this bar is a thin strip, not an overlay on top of a
large video frame. */
.musicBarPresence .avatar {
width: 26px;
height: 26px;
font-size: 10px;
}
.musicBarImage {
width: 40px;
height: 40px;
object-fit: cover;
border-radius: 6px;
background: var(--panel);
flex-shrink: 0;
}
.musicBarImage.noImage { visibility: hidden; }
.musicBarInfo {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
margin-right: auto;
}
.musicBarInfo strong {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 13px;
}
.musicBarPosition { font-variant-numeric: tabular-nums; white-space: nowrap; }
/* #castBtn while actively casting - same "clearly-on" treatment as
views/player/Player.css's own .iconBtn[aria-pressed="true"] (ID-scoped
here rather than a shared .iconBtn rule since #playPauseBtn's own
"iconBtn" class in this bar has no matching style at all - see
MusicBar.html - and never sets aria-pressed, so this couldn't touch it
either way, but there's no shared iconBtn.css to hang a rule off yet). */
#castBtn[aria-pressed="true"] {
color: #ff9800;
background: rgba(255, 152, 0, .18);
}
/* Pops up ABOVE the bar (same pattern as Player.css's .settingsPanel above
its gear button) - collapsed by default, see MusicBar.ts's toggleQueueBtn
handler. Spans the full bar width (no max-width) rather than a fixed
popup box - a party's queue can run long, and a narrow column wastes
the width a desktop viewport already has going spare.
display:flex/flex-direction:column here (rather than on .pageHeader/
#queueList individually) is what lets #queueList be the only child
that scrolls below - see #queueList's own comment. */
.musicBarQueue {
max-height: 50vh;
margin: 0 16px;
padding: 14px 14px 0;
background: var(--bg-header);
border: 1px solid rgba(255, 255, 255, .08);
border-bottom: none;
border-radius: 10px 10px 0 0;
display: flex;
flex-direction: column;
}
.musicBarQueue .pageHeader { flex-shrink: 0; }
/* The only scrolling region inside .musicBarQueue - "File d'attente"
above it stays put instead of scrolling away with the tracks. */
#queueList {
overflow-y: auto;
}
@media (max-width: 720px) {
.musicBarRow { justify-content: center; }
.musicBarInfo { margin-right: 0; flex-basis: 100%; order: -1; text-align: center; }
.musicBarQueue { margin: 0; border-radius: 0; }
}
/* One queue entry - a plain flex row (image, then a shrinking/truncating
info column, then the duration), NOT views/album's shared .trackRow
grid (styles/trackList.css) - that one's 4 fixed columns assume
Album.ts's own index/name/duration/button shape, which crushed this
list's track names into its 28px index column when reused here. See
MusicBar.ts's own comment. */
.queueRow {
display: flex;
align-items: center;
gap: 10px;
background: var(--panel);
border-radius: 8px;
padding: 8px 10px;
}
.queueRowArt {
width: 38px;
height: 38px;
object-fit: cover;
border-radius: 6px;
background: var(--bg-header);
flex-shrink: 0;
}
.queueRowArt.noImage { visibility: hidden; }
.queueRowInfo {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
flex: 1;
}
.queueRowName {
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* Artist/album/"Ajouté par X" condensed onto one secondary line (see
MusicBar.ts) rather than each getting their own column - there's no
room for 3+ columns once a name needs to read in full, unlike
Album.ts's simpler index/name/duration/button row. */
.queueRowMeta {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}