All checks were successful
Publier l'image Docker / publish (push) Successful in 17s
1. Carousel/grid tiles had inconsistent widths - classic flexbox/grid "min-width: auto" pitfall: a long title (or an image's intrinsic size before load) could force a tile wider than its declared flex-basis/track size. Fixed with min-width: 0 on .tile. Verified: three tiles (one with a deliberately very long title) now measure exactly 160px each. 2. Player language/subtitle/quality controls were three always-visible dropdowns floating over the video. Moved behind a small gear-icon button in the action bar that opens a compact popup panel (closes on outside click or on selecting an option) - host-only, same as before. 3. Sidebar library entries no longer get an emoji prefix - plain names. 4. Player failures were silent (a caught error with just a code comment, or hls.js failing with nothing shown). Added: a loading notice while the stream loads, an on-screen error box (with a Réessayer button) for fatal hls.js errors and native <video> errors, and console.error logging of the technical detail. Confirmed working live during testing - a real hls.js fatal error (no live Emby stream available in the test environment) was caught and displayed correctly instead of failing silently. This is meant to turn "the player doesn't work" into an actual diagnosable error message next time it happens for real. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
529 lines
11 KiB
CSS
529 lines
11 KiB
CSS
/* Colors pulled live from this Emby server's own active theme (dark), via
|
|
its CSS custom properties (--theme-primary-color-*, --background-*, etc.)
|
|
- see docs/architecture.md for how these were read directly off the
|
|
running server rather than guessed. */
|
|
:root {
|
|
color-scheme: dark;
|
|
--bg: #151216; /* hsl(285, 10%, 7.84%) - Emby's dark theme background */
|
|
--bg-header: #221d24; /* same hue, +5% lightness - Emby's header/footer background */
|
|
--panel: #262626; /* Emby's card/button/input background (achromatic, ~15-17% lightness) */
|
|
--text: #ffffff;
|
|
--muted: rgba(255, 255, 255, .6);
|
|
--accent: #52b54b; /* Emby's theme-primary-color (hsl(116, 41.7%, 50.2%)) */
|
|
--accent-bright: #6ccf65; /* Emby's --theme-accent-text-color-darkbg */
|
|
--danger: #ff6b6b;
|
|
}
|
|
|
|
* { box-sizing: border-box; }
|
|
|
|
html, body {
|
|
margin: 0;
|
|
height: 100%;
|
|
}
|
|
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
}
|
|
|
|
#app { min-height: 100%; }
|
|
|
|
/* Login is a small centered form - everything else lives inside the app
|
|
shell (sidebar + .mainContent), see below. */
|
|
.wrap {
|
|
max-width: 420px;
|
|
margin: 0 auto;
|
|
padding: 32px 20px 80px;
|
|
}
|
|
|
|
h1 { font-size: 22px; margin: 0; }
|
|
h2 { font-size: 16px; margin: 0 0 12px; }
|
|
|
|
.panel {
|
|
background: var(--bg-header);
|
|
border-radius: 12px;
|
|
padding: 20px;
|
|
}
|
|
|
|
.hint { color: var(--muted); font-size: 13px; margin: 0 0 16px; }
|
|
.error { color: var(--danger); font-size: 13px; min-height: 18px; }
|
|
|
|
label {
|
|
display: block;
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
input {
|
|
display: block;
|
|
width: 100%;
|
|
margin-top: 4px;
|
|
padding: 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid #3a3a3a;
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
font-size: 14px;
|
|
}
|
|
|
|
input:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
button {
|
|
padding: 12px 16px;
|
|
border: none;
|
|
border-radius: 8px;
|
|
background: var(--accent);
|
|
color: white;
|
|
font-size: 14px;
|
|
font-weight: 600;
|
|
cursor: pointer;
|
|
}
|
|
|
|
button:hover { background: var(--accent-bright); }
|
|
button[disabled] { opacity: .5; cursor: default; }
|
|
|
|
button.linklike {
|
|
background: none;
|
|
color: var(--muted);
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
text-decoration: underline;
|
|
padding: 4px 0;
|
|
}
|
|
|
|
button.linklike:hover { background: none; color: var(--accent-bright); }
|
|
|
|
.code { color: var(--accent-bright); letter-spacing: .1em; }
|
|
|
|
/* ---- App shell: sidebar + routed main content ---- */
|
|
|
|
.appShell {
|
|
display: flex;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
.sidebar {
|
|
width: 220px;
|
|
flex-shrink: 0;
|
|
background: var(--bg-header);
|
|
padding: 20px 12px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
position: sticky;
|
|
top: 0;
|
|
height: 100vh;
|
|
overflow-y: auto;
|
|
}
|
|
|
|
.sidebarBrand {
|
|
font-size: 18px;
|
|
font-weight: 700;
|
|
padding: 0 12px 20px;
|
|
}
|
|
|
|
.sidebarNav {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
flex: 1;
|
|
}
|
|
|
|
.navSectionLabel {
|
|
color: var(--muted);
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: .05em;
|
|
padding: 16px 12px 6px;
|
|
}
|
|
|
|
.navItem {
|
|
display: block;
|
|
padding: 10px 12px;
|
|
border-radius: 8px;
|
|
color: var(--text);
|
|
text-decoration: none;
|
|
font-size: 14px;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.navItem:hover { background: rgba(255, 255, 255, .06); }
|
|
.navItem.active { background: var(--accent); color: white; font-weight: 600; }
|
|
|
|
.sidebarLogout { margin-top: 12px; padding: 8px 12px; }
|
|
|
|
.mainContent {
|
|
flex: 1;
|
|
min-width: 0;
|
|
padding: 24px clamp(16px, 3vw, 48px) 80px;
|
|
}
|
|
|
|
/* Sidebar becomes a horizontal scrollable topbar on narrow screens - same
|
|
"scroll instead of wrap" treatment as .tabs below. */
|
|
@media (max-width: 720px) {
|
|
.appShell { flex-direction: column; }
|
|
.sidebar {
|
|
width: 100%;
|
|
height: auto;
|
|
flex-direction: row;
|
|
align-items: center;
|
|
padding: 10px 12px;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
}
|
|
.sidebarBrand { padding: 0 12px 0 0; }
|
|
.sidebarNav { flex-direction: row; flex: none; }
|
|
.navSectionLabel { display: none; }
|
|
.sidebarLogout { margin: 0 0 0 12px; }
|
|
}
|
|
|
|
.pageHeader {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
gap: 12px;
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
.joinInline { display: flex; gap: 8px; align-items: center; }
|
|
.joinInline input { width: 140px; margin: 0; }
|
|
.joinInline button { padding: 10px 14px; }
|
|
|
|
.section { margin-bottom: 32px; }
|
|
|
|
/* ---- Horizontal carousels (home page) ---- */
|
|
|
|
.carousel {
|
|
display: flex;
|
|
gap: 16px;
|
|
overflow-x: auto;
|
|
padding-bottom: 10px;
|
|
}
|
|
|
|
.carousel .tile { flex: 0 0 160px; }
|
|
.carousel .partyCard { flex: 0 0 280px; }
|
|
|
|
.partyCard {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 12px;
|
|
background: var(--bg-header);
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.partyCard img {
|
|
width: 48px;
|
|
height: 72px;
|
|
object-fit: cover;
|
|
border-radius: 6px;
|
|
background: var(--panel);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.partyCard img.noImage { visibility: hidden; }
|
|
|
|
.partyCardInfo {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 2px;
|
|
}
|
|
|
|
.partyCardInfo strong { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
|
.partyCard button { flex-shrink: 0; padding: 8px 14px; font-size: 13px; }
|
|
|
|
/* Tabs: one per Emby library (library.js) or per season (series.js). */
|
|
.tabs {
|
|
display: flex;
|
|
gap: 8px;
|
|
overflow-x: auto;
|
|
padding-bottom: 8px;
|
|
margin-bottom: 20px;
|
|
border-bottom: 1px solid rgba(255,255,255,.1);
|
|
}
|
|
|
|
.tab {
|
|
background: none;
|
|
color: var(--muted);
|
|
font-weight: 600;
|
|
padding: 8px 4px;
|
|
border-radius: 0;
|
|
white-space: nowrap;
|
|
border-bottom: 2px solid transparent;
|
|
}
|
|
|
|
.tab:hover { background: none; color: var(--text); }
|
|
.tab.active { color: var(--accent-bright); border-bottom-color: var(--accent-bright); }
|
|
|
|
/* ---- Tile grid (library.js) + tiles (shared with the carousels) ---- */
|
|
|
|
.grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
gap: 20px 16px;
|
|
}
|
|
|
|
.tile {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
/* Without this, a long title (or an image's intrinsic size before it
|
|
loads) can force this flex/grid item wider than its declared size -
|
|
the classic "min-width: auto" flexbox/grid pitfall. Every tile must
|
|
stay exactly the track/flex-basis size regardless of content. */
|
|
min-width: 0;
|
|
}
|
|
|
|
.tile img {
|
|
width: 100%;
|
|
aspect-ratio: 2 / 3;
|
|
object-fit: cover;
|
|
border-radius: 8px;
|
|
background: var(--panel);
|
|
display: block;
|
|
}
|
|
|
|
.tile img.noImage { visibility: hidden; }
|
|
|
|
.tileLink {
|
|
position: absolute;
|
|
inset: 0 0 26px 0;
|
|
z-index: 1;
|
|
}
|
|
|
|
.tileLabel {
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
white-space: nowrap;
|
|
}
|
|
|
|
/* Hover-reveal overlay with the two actions, on devices that actually have
|
|
hover (mouse/trackpad). Touch devices get an always-visible bar instead -
|
|
see the (hover: none) block below, since :hover is unreachable there. */
|
|
.tileOverlay {
|
|
position: absolute;
|
|
inset: 0 0 26px 0;
|
|
z-index: 2;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: stretch;
|
|
justify-content: center;
|
|
gap: 8px;
|
|
padding: 10px;
|
|
background: rgba(0, 0, 0, .55);
|
|
border-radius: 8px;
|
|
opacity: 0;
|
|
transition: opacity .15s ease;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.tile:hover .tileOverlay,
|
|
.tile:focus-within .tileOverlay {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
}
|
|
|
|
.tileBtn {
|
|
padding: 8px 10px;
|
|
font-size: 12px;
|
|
text-align: center;
|
|
text-decoration: none;
|
|
}
|
|
|
|
.tileBtnLink {
|
|
background: var(--panel);
|
|
color: var(--text);
|
|
border-radius: 8px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.tileBtnLink:hover { background: #333; }
|
|
|
|
@media (hover: none) {
|
|
.tileOverlay {
|
|
opacity: 1;
|
|
pointer-events: auto;
|
|
inset: auto 0 26px 0;
|
|
flex-direction: row;
|
|
background: linear-gradient(to top, rgba(0,0,0,.75), transparent);
|
|
padding: 6px;
|
|
}
|
|
.tileBtn { font-size: 11px; padding: 6px 4px; }
|
|
}
|
|
|
|
/* ---- Series page: seasons tabs + episode list ---- */
|
|
|
|
.episodeList {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
}
|
|
|
|
.episodeRow {
|
|
display: flex;
|
|
gap: 14px;
|
|
align-items: center;
|
|
background: var(--bg-header);
|
|
border-radius: 10px;
|
|
padding: 10px;
|
|
}
|
|
|
|
.episodeRow img {
|
|
width: 120px;
|
|
aspect-ratio: 16 / 9;
|
|
object-fit: cover;
|
|
border-radius: 6px;
|
|
background: var(--panel);
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.episodeRow img.noImage { visibility: hidden; }
|
|
|
|
.episodeInfo { flex: 1; min-width: 0; }
|
|
.episodeInfo strong { display: block; margin-bottom: 4px; }
|
|
.episodeInfo p {
|
|
margin: 0;
|
|
color: var(--muted);
|
|
font-size: 13px;
|
|
overflow: hidden;
|
|
display: -webkit-box;
|
|
-webkit-line-clamp: 2;
|
|
-webkit-box-orient: vertical;
|
|
}
|
|
|
|
.episodeRow button { flex-shrink: 0; }
|
|
|
|
@media (max-width: 600px) {
|
|
.episodeRow { flex-wrap: wrap; }
|
|
.episodeInfo { order: 3; flex-basis: 100%; }
|
|
}
|
|
|
|
/* ---- Player ---- */
|
|
|
|
.playerWrap {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100vh;
|
|
background: #000;
|
|
}
|
|
|
|
.playerWrap video {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: contain;
|
|
}
|
|
|
|
.overlay {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 16px;
|
|
padding: 12px 16px;
|
|
background: linear-gradient(to bottom, rgba(0,0,0,.7), transparent);
|
|
color: white;
|
|
font-size: 13px;
|
|
flex-wrap: wrap;
|
|
}
|
|
|
|
.overlay .itemTitle { font-weight: 600; }
|
|
.overlay .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); }
|
|
|
|
.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;
|
|
}
|
|
|
|
.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; }
|
|
|
|
.followerNotice {
|
|
position: absolute;
|
|
bottom: 24px;
|
|
left: 50%;
|
|
transform: translateX(-50%);
|
|
background: rgba(0,0,0,.6);
|
|
padding: 8px 16px;
|
|
border-radius: 20px;
|
|
font-size: 13px;
|
|
color: var(--muted);
|
|
}
|
|
|
|
.hide { display: none; }
|