watchparty/client/src/views/player/Player.css
Nicolas 26d32632c7
Some checks failed
Publier l'image Docker / test (pull_request) Failing after 15s
Publier l'image Docker / publish (pull_request) Has been skipped
Hide the cursor over the video once the bottom bar auto-hides
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>
2026-09-15 22:50:08 +02:00

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; }