// ─── Shared mixin for live-stream portal layouts ────────────────────────────── // Used by: xtream live-stream-layout and stalker live-stream-layout. // // Include this mixin in a component's SCSS to get the shared flex host+sidebar // skeleton, then add only the component-specific rules underneath. // // Choose the relative @use path from each consuming stylesheet. Current portal // live layouts use the example below; other consumers may need a different // depth. // @use '../../../../../../ui/styles/portal-layout' as portal; // @include portal.live-layout; @mixin live-layout { :host { display: flex; flex-direction: row; overflow: hidden; height: 100%; width: 100%; } // ─── Sidebar ───────────────────────────────────────────────────────────── .sidebar { // Width controlled by the resizable directive at runtime width: 400px; box-sizing: border-box; padding-inline-end: 8px; min-width: 250px; max-width: 600px; border-right: 1px solid var(--app-separator); display: flex; flex-direction: column; flex-shrink: 0; overflow: hidden; position: relative; container-type: inline-size; transition: width 0.18s ease, min-width 0.18s ease, border-color 0.18s ease; &.sidebar-collapsed { // Beats the inline width set by the resizable directive while // preserving it so uncollapsing snaps back to the previous size. width: 0 !important; padding-inline-end: 0; min-width: 0 !important; border-right-color: transparent; } &.sidebar-collapsed .resize-handle { display: none; } app-category-view { flex: 1; overflow-y: auto; overflow-x: hidden; margin-right: 2px; } } // ─── Main content column ───────────────────────────────────────────────── .content-container { flex: 1; display: flex; flex-direction: column; overflow: hidden; min-height: 0; position: relative; } // ─── Floating restore handle (visible when sidebar is collapsed) ───────── // Anchored top-left of .content-container so it sits in roughly the same // spot as the hide toggle that lived at the sidebar header's top-right. .sidebar-restore { position: absolute; top: 6px; left: 6px; // Player internals, including diagnostic overlays, may use z-index 5 // inside the content area. Keep the restore affordance above them for // both paint order and hit testing. z-index: 6; background: rgba(128, 128, 128, 0.18); color: var(--mat-sys-on-surface, rgba(255, 255, 255, 0.75)); box-shadow: none; opacity: 0.7; transition: opacity 0.15s ease, background 0.15s ease; .mat-mdc-button-persistent-ripple, .mat-mdc-button-touch-target { display: none; } mat-icon { font-size: 20px; width: 20px; height: 20px; line-height: 20px; } &:hover, &:focus-visible { opacity: 1; background: rgba(128, 128, 128, 0.32); } } .video-player { flex: 1 1 0; min-height: 0; overflow: hidden; display: flex; > app-web-player-view { flex: 1; min-height: 0; } } .epg { flex: 1 1 0; min-height: 0; overflow: hidden; display: flex; flex-direction: column; transition: flex-basis 0.18s ease, min-height 0.18s ease; // Inline (player visible): the guide is a compact panel below the // player, not half the screen. The responsive clamp keeps the player // dominant on tall screens and shrinks the panel on short ones. // External player: no .epg--inline → keeps flex:1 above and fills. &.epg--inline { flex: 0 0 var(--epg-inline-height, clamp(180px, 36vh, 264px)); } // Vertical list view (epgViewMode = 'list'): rows need more height to // be useful — raise the inline clamp only. Sets just the variable, so // the later `.epg-collapsed` flex rule still wins when collapsed. &.epg--list { --epg-inline-height: clamp(280px, 46vh, 430px); } &.epg-collapsed { flex: 0 0 56px; min-height: 56px; } > app-epg-timeline, > app-epg-list-view { flex: 1; min-height: 0; } } .epg-content { height: 100%; min-height: 0; overflow: hidden; display: flex; flex-direction: column; > app-epg-timeline, > app-epg-list-view { flex: 1; min-height: 0; } } // ─── Fix artplayer sizing when nested inside .video-player ─────────────── ::ng-deep { .video-player > app-web-player-view > app-art-player > .artplayer-container { height: 100% !important; width: 100% !important; } } // ─── Phone layout ──────────────────────────────────────────────────────── // Side by side, the 400px channel list leaves nothing for the player on a // phone, so the two stack instead. The width needs `!important` because // ResizableDirective writes the persisted desktop width inline; dragging // is meaningless at full width, so the handle goes too. @media (max-width: 640px) { :host { flex-direction: column; } .sidebar { padding-inline-end: 0; width: 100% !important; min-width: 0 !important; max-width: none; flex-shrink: 1; max-height: 42vh; // Without a floor of its own the player's minimum can take the // whole body on a short screen and the list vanishes while still // marked expanded, leaving no way to pick another channel. min-height: 72px; border-right: none; border-bottom: 1px solid var(--app-separator); &.sidebar-collapsed { max-height: 0; min-height: 0; border-bottom-color: transparent; } .resize-handle { display: none; } } .content-container { flex: 1 1 auto; } // Inside that container the guide is `flex: 0 0 ` while the // video is `flex: 1 1 0`, so shrinking is distributed by basis and // the video — basis zero — absorbed none of it and collapsed. Here // the video keeps a floor and the guide is what gives way. .video-player { min-height: 96px; } .epg.epg--inline { flex-shrink: 1; min-height: 0; } } // The player's floor, not the list's ceiling, is what has to hold — but // only where the screen can afford it. A landscape phone already spends // 30vh on the stacked categories panel, so claiming 240px more here left // nothing for the channel list; there, the two panes just share what is // left. Keyed on height because that, not width, is what runs out. @media (max-width: 640px) and (min-height: 600px) { .content-container { min-height: 240px; } } }