Asked for directly: the auto-hide behavior itself (bottom bar collapses to just its seek line after a stretch of no activity) already shipped earlier - a mouse cursor left sitting over the video afterward is the same kind of clutter that was the point of hiding in the first place. .playerWrap now gets a .cursorHidden class alongside #bottomBar's own .minimal, toggled at the exact same two call sites (scheduleControlsHide/showControls) so the two states can never drift out of sync with each other. cursor: none on that class does the rest - no new listener needed, since playerWrap's own pointermove/pointerdown already call showControls() on any mouse activity, which is exactly when the cursor should reappear too. Verified: typecheck + build (client). Confirmed the compiled bundle carries both the classList calls and the CSS rule. Not verified in an actual running player - no Emby login reachable from this sandbox to open a real video party and let the auto-hide timer elapse. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
397 lines
11 KiB
CSS
397 lines
11 KiB
CSS
.playerWrap {
|
|
/* Fixed + inset so it fills the actual viewport regardless of
|
|
.mainContent's own padding (which used to add to 100vh and push the
|
|
whole page taller than the screen, causing an outer scrollbar). Also
|
|
covers the sidebar while playing, like most video players' theater
|
|
mode - "Quitter" is how you get back to it. */
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 20;
|
|
background: #000;
|
|
}
|
|
|
|
.playerWrap video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
/* Toggled alongside .bottomBar.minimal (see Player.ts's scheduleControlsHide/
|
|
showControls) - a mouse cursor left sitting over the video is the same
|
|
kind of clutter the bar itself was, once there's nothing left under it
|
|
to interact with. Any pointermove/pointerdown anywhere in .playerWrap
|
|
already calls showControls(), so the cursor reappears the instant the
|
|
mouse moves again - same convention as any other video player's
|
|
auto-hiding controls. */
|
|
.playerWrap.cursorHidden { cursor: none; }
|
|
|
|
/* Two stacked rows, both part of the same top gradient overlay: the action
|
|
bar (title/members/next/settings/leave) and, below it, the presence
|
|
avatars - see .presenceOverlay in styles/presence.css. Stacking them in
|
|
normal flow inside one column means the avatar row always ends up right
|
|
after the action bar regardless of how tall that bar is (it wraps to
|
|
two lines on narrow screens), instead of guessing a fixed pixel offset
|
|
for a second absolutely-positioned block. */
|
|
.topOverlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
padding: 12px 16px;
|
|
background: linear-gradient(to bottom, rgba(0,0,0,.7), transparent);
|
|
z-index: 5;
|
|
}
|
|
|
|
.actionBar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
color: white;
|
|
font-size: 13px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.actionBar .itemTitle { font-weight: 600; }
|
|
.actionBar .members { color: var(--muted); flex: 1; }
|
|
|
|
/* Settings: a small gear icon in the action bar, not a permanently-visible
|
|
block of dropdowns - opens a compact popup on click. */
|
|
.settingsWrap { position: relative; }
|
|
|
|
.iconBtn {
|
|
background: none;
|
|
padding: 4px 8px;
|
|
font-size: 16px;
|
|
line-height: 1;
|
|
border-radius: 6px;
|
|
}
|
|
|
|
.iconBtn:hover { background: rgba(255, 255, 255, .12); }
|
|
|
|
/* #castBtn while actively casting - see Player.ts's toggleCast. Same
|
|
"clearly-on" treatment as .avatarActionRequested's orange, not just a
|
|
background tint, so it reads at a glance whether the TV or this screen
|
|
is the one actually playing right now. */
|
|
.iconBtn[aria-pressed="true"] {
|
|
color: #ff9800;
|
|
background: rgba(255, 152, 0, .18);
|
|
}
|
|
|
|
.settingsPanel {
|
|
position: absolute;
|
|
top: 100%;
|
|
right: 0;
|
|
margin-top: 6px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
background: var(--bg-header);
|
|
padding: 12px;
|
|
border-radius: 10px;
|
|
font-size: 12px;
|
|
min-width: 180px;
|
|
box-shadow: 0 8px 24px rgba(0, 0, 0, .4);
|
|
z-index: 5;
|
|
}
|
|
|
|
.settingsPanel label {
|
|
color: var(--muted);
|
|
margin-bottom: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
|
|
.settingsPanel select {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border: 1px solid #3a3a3a;
|
|
border-radius: 6px;
|
|
padding: 4px 6px;
|
|
font-size: 12px;
|
|
}
|
|
|
|
.settingsPanel select:disabled {
|
|
opacity: .5;
|
|
}
|
|
|
|
/* .hint's own bottom margin (base.css) would stack with the panel's own
|
|
flex `gap` below it - zeroed here so it's spaced exactly like every
|
|
other row in the column, not a wider gap after this specific one. */
|
|
.burnInHint {
|
|
margin: 0;
|
|
max-width: 220px;
|
|
}
|
|
|
|
.loadingNotice {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
color: var(--muted);
|
|
font-size: 14px;
|
|
margin: 0;
|
|
}
|
|
|
|
.playerError {
|
|
position: absolute;
|
|
top: 50%;
|
|
left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
max-width: 360px;
|
|
text-align: center;
|
|
background: rgba(0, 0, 0, .75);
|
|
padding: 20px;
|
|
border-radius: 10px;
|
|
}
|
|
|
|
.playerError p { color: var(--danger); font-size: 14px; margin: 0 0 10px; }
|
|
|
|
/* Transient toast (see showRequestBanner in Player.ts) - positioned
|
|
independently near the top so it never fights the persistent
|
|
.bottomBar below for space, and reads as a pop-in/fade-out
|
|
notification rather than a fourth thing bolted onto a fixed layout.
|
|
Centered under .topOverlay's usual (single-line) height - see
|
|
Player.html's own comment. */
|
|
.requestBanner {
|
|
position: absolute;
|
|
top: 64px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
z-index: 7;
|
|
max-width: calc(100% - 32px);
|
|
text-align: center;
|
|
background: rgba(0, 0, 0, .75);
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 13px;
|
|
}
|
|
|
|
/* Replaces what used to be three separate floating pieces (a centered
|
|
banner/notice column, a request toolbar cornered bottom-right, an
|
|
"Épisode suivant" link in the top bar) AND the browser's native
|
|
<video controls> with one persistent bar - see Player.html's own
|
|
comment on why staying inside .playerWrap (not a sibling positioned
|
|
relative to it) also matters for plein écran. Column layout (seek
|
|
bar row, then buttons row) so the two stack in normal flow instead of
|
|
two independently-guessed fixed offsets - same "stack, don't guess a
|
|
pixel offset" fix .topOverlay's own actionBar+presenceOverlay
|
|
grouping already needed (see its own comment). */
|
|
.bottomBar {
|
|
position: absolute;
|
|
left: 10px;
|
|
right: 10px;
|
|
bottom: 10px;
|
|
z-index: 6;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: rgba(10, 8, 11, .8);
|
|
border: 1px solid rgba(255, 255, 255, .08);
|
|
border-radius: 12px;
|
|
padding: 10px;
|
|
transition: padding .25s ease, background-color .25s ease, border-color .25s ease;
|
|
}
|
|
|
|
/* Collapsed down to just the seek bar after a stretch of no activity
|
|
(see Player.ts's scheduleControlsHide) - asked for directly: the
|
|
full bar, always visible, sits right where a native <track> subtitle
|
|
renders (applySubtitleTrack) and was masking it. Only the padding/
|
|
background/border and the buttons row transition - #progressBar
|
|
itself (and its own generous invisible hit-area, see below) stays
|
|
exactly where and how big it already was either way. */
|
|
.bottomBar.minimal {
|
|
padding: 0;
|
|
background: transparent;
|
|
border-color: transparent;
|
|
}
|
|
|
|
.bottomBarButtons {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
margin-top: 8px; /* not .bottomBar's own gap - so .minimal can collapse just this row's own spacing, not the seek bar's position */
|
|
max-height: 40px;
|
|
opacity: 1;
|
|
overflow: hidden;
|
|
transition: max-height .25s ease, opacity .2s ease, margin-top .25s ease;
|
|
}
|
|
|
|
.bottomBar.minimal .bottomBarButtons {
|
|
max-height: 0;
|
|
opacity: 0;
|
|
margin-top: 0;
|
|
pointer-events: none; /* never clickable while invisible - see Player.ts's own comment on why this needs no extra guarding at the click handlers themselves */
|
|
}
|
|
.bottomBarZone { display: flex; align-items: center; gap: 8px; }
|
|
.bottomBarZone.left { flex-shrink: 0; }
|
|
.bottomBarZone.center { flex: 1; justify-content: center; min-width: 0; }
|
|
.bottomBarZone.right { flex-shrink: 0; }
|
|
|
|
/* Host: click/drag to seek (Player.ts). Follower: read-only, just a
|
|
position indicator - same "everyone sees the fill, only the host's
|
|
input actually does anything" split as the request buttons vs.
|
|
#ambientNotice above. The generous ::before hit-area (not just the
|
|
4px visual track) matters most for drag on a touch screen. */
|
|
.progressBar {
|
|
position: relative;
|
|
height: 4px;
|
|
border-radius: 3px;
|
|
background: rgba(255, 255, 255, .18);
|
|
cursor: pointer;
|
|
touch-action: none; /* this app's own pointer handlers drive the drag, not the browser's default touch scroll/zoom gestures */
|
|
}
|
|
|
|
.progressBar.readOnly { cursor: default; }
|
|
|
|
.progressBar::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: -8px;
|
|
bottom: -8px;
|
|
left: 0;
|
|
right: 0;
|
|
}
|
|
|
|
.progressFill {
|
|
position: absolute;
|
|
top: 0;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 0%;
|
|
background: var(--accent);
|
|
border-radius: 3px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.timeLabel {
|
|
color: var(--muted);
|
|
font-size: 11.5px;
|
|
font-variant-numeric: tabular-nums;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Center zone: never more than one message at a time (see
|
|
updateAmbientNotice in Player.ts) - "L'hôte contrôle la lecture" for
|
|
a follower, or "Diffusion en cours sur le Chromecast" while casting
|
|
(that one takes priority - the more useful thing to know in the
|
|
moment), empty for a host who isn't casting. */
|
|
.ambientNotice {
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
/* Nested in .bottomBarZone.left now, not its own cornered overlay - see
|
|
Player.html. Never shown alongside #nextBtn below - that same zone
|
|
shows exactly one of the two (host vs follower), never both. */
|
|
.requestToolbar {
|
|
display: flex;
|
|
gap: 8px;
|
|
}
|
|
|
|
.requestBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: rgba(255, 255, 255, .1);
|
|
color: white;
|
|
padding: 7px 12px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.requestBtn:hover { background: rgba(255, 255, 255, .18); }
|
|
.requestBtn svg { flex-shrink: 0; }
|
|
|
|
/* Host-only, sharing the request buttons' left zone (see .requestToolbar
|
|
above) - a real pill button now instead of the old plain underlined
|
|
top-bar link. Accent green, not the neutral grey .requestBtn uses -
|
|
this is a direct, primary action, not a muted ask. */
|
|
.nextBtn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 7px;
|
|
background: var(--accent);
|
|
color: white;
|
|
padding: 7px 14px;
|
|
border-radius: 20px;
|
|
font-size: 12px;
|
|
font-weight: 700;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
.nextBtn:hover { background: var(--accent-bright); }
|
|
.nextBtn svg { flex-shrink: 0; }
|
|
|
|
.fsBtn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 30px;
|
|
height: 30px;
|
|
padding: 0; /* base.css's own `button` rule defaults to 12px 16px - wider than this button's declared box, and a flex item's default min-width:auto would otherwise let that padding grow it past 30px */
|
|
border-radius: 8px;
|
|
background: rgba(255, 255, 255, .1);
|
|
color: white;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.fsBtn:hover { background: rgba(255, 255, 255, .18); }
|
|
|
|
/* Replaces the video content while casting - the local <video> is
|
|
paused/torn down (see Player.ts's startCasting/cast.ts), so leaving
|
|
its last frame frozen with no explanation would just look stuck. */
|
|
.castingScreen {
|
|
position: absolute;
|
|
inset: 0;
|
|
z-index: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
color: rgba(255, 255, 255, .4);
|
|
text-align: center;
|
|
}
|
|
|
|
.castingScreen strong {
|
|
color: rgba(255, 255, 255, .85);
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
/* Shown to EVERY member (including the host and the requester themselves)
|
|
on the matching person's circle while their pause/resume request is
|
|
active - see onPauseRequested()/onResumeRequested() in Player.ts.
|
|
Orange border + a small icon badge, not just a generic pulse, so it
|
|
reads unambiguously even to someone glancing at the video rather than
|
|
the banner text. Same treatment for either request - the banner text
|
|
is what says which one. */
|
|
.avatarActionRequested {
|
|
opacity: 1;
|
|
border-color: #ff9800;
|
|
}
|
|
|
|
.avatarRequestBadge {
|
|
position: absolute;
|
|
bottom: -4px;
|
|
right: -4px;
|
|
width: 16px;
|
|
height: 16px;
|
|
border-radius: 50%;
|
|
background: #ff9800;
|
|
border: 1px solid rgba(0, 0, 0, .5);
|
|
display: none;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.avatarActionRequested .avatarRequestBadge { display: flex; }
|