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>
176 lines
5.1 KiB
CSS
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;
|
|
}
|