From 18c82144a4e05cadd0803b9917c37eb85555ad2c Mon Sep 17 00:00:00 2001 From: 4gray Date: Fri, 31 Jul 2026 10:22:05 +0200 Subject: [PATCH] fix(ui): ignore hidden panels in master shortcut --- .../src/live-panel-toggles.e2e.ts | 10 +++- ...026-07-31-live-panel-toggle-consistency.md | 25 ++++---- .../video-player.component.spec.ts | 23 ++++++++ .../video-player/video-player.component.ts | 15 +++-- .../live-layout-panel-state.service.spec.ts | 58 +++++++++---------- .../lib/live-layout-panel-state.service.ts | 14 ++--- .../unified-collection-page.component.ts | 6 +- ...alker-live-stream-layout.component.spec.ts | 19 ++++++ .../stalker-live-stream-layout.component.ts | 26 ++++++++- .../live-stream-layout.component.ts | 26 ++++++++- 10 files changed, 155 insertions(+), 67 deletions(-) diff --git a/apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts b/apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts index 9ef6b7f4e..7f75a26a0 100644 --- a/apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts +++ b/apps/electron-backend-e2e/src/live-panel-toggles.e2e.ts @@ -123,9 +123,17 @@ test('@live-panels @m3u @electron keeps Groups and Channels independent, persist ) .toBe('collapsed'); + await showPanel(page, 'groups'); + await expectPanelState(page, 'groups', true); + await expectPanelState(page, 'channels', false); await page.setViewportSize({ width: 580, height: 900 }); await expect(panelControl(page, 'groups', 'restore')).toHaveCount(0); await expect(panelControl(page, 'channels', 'restore')).toBeVisible(); + await page.keyboard.press(masterShortcut()); + await page.setViewportSize({ width: 1280, height: 720 }); + await expectPanelState(page, 'groups', true); + await expectPanelState(page, 'channels', false); + await page.setViewportSize({ width: 580, height: 900 }); await showPanel(page, 'channels'); await expectPanelState(page, 'channels', true); } finally { @@ -296,7 +304,7 @@ async function showPanel( const control = panelControl(page, panel, 'restore'); await expect(control).toBeVisible(); await expect(control).toHaveAttribute('aria-expanded', 'false'); - await control.click(); + await control.press('Enter'); } async function expectPanelState( diff --git a/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md b/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md index 8a2a66010..08bb13c4d 100644 --- a/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md +++ b/docs/superpowers/plans/2026-07-31-live-panel-toggle-consistency.md @@ -88,7 +88,7 @@ expect( responsiveSuppressed: true, }) ).toBe(false); -service.toggleMasterSuppression(['groups', 'channels']); +service.toggleMasterSuppression(['groups']); expect(service.masterSuppressed()).toBe(true); service.showPanel('channels'); expect(service.masterSuppressed()).toBe(false); @@ -144,7 +144,9 @@ export class LiveLayoutPanelStateService { hidePanel(panel: LiveLayoutPanel): void; showPanel(panel: LiveLayoutPanel): void; - toggleMasterSuppression(applicablePanels: readonly LiveLayoutPanel[]): void; + toggleMasterSuppression( + effectivelyVisiblePanels: readonly LiveLayoutPanel[] + ): void; } ``` @@ -447,13 +449,16 @@ Inject `LiveLayoutPanelStateService`, compute Channels effective visibility for ```ts this.livePanelState.toggleMasterSuppression( - this.showLiveChannelSidebar() ? ['groups', 'channels'] : ['groups'] + this.effectivelyVisibleLeftPanels() ); ``` Keep the resizable Channels DOM mounted and inert when collapsed. Move restore UI into a non-overlay boundary rail before `.content-container`. Pass `[collapsible]="isEmbeddedPlayer"` and `panelId="live-guide-panel"`. +The shortcut helper must resolve Groups with the workspace `1023px` +responsive suppression and include Channels only when its effective state is +expanded; persisted intent alone must not manufacture a visible panel. - [x] **Step 4: Apply the same contract to Stalker** @@ -753,7 +758,7 @@ git commit -m "docs(ui): document live panel ownership" - Review every file changed relative to `origin/master`. -- [ ] **Step 1: Run affected unit targets** +- [x] **Step 1: Run affected unit targets** ```bash pnpm nx run-many \ @@ -764,7 +769,7 @@ pnpm nx run-many \ Expected: all affected unit targets pass. -- [ ] **Step 2: Run affected lint** +- [x] **Step 2: Run affected lint** ```bash pnpm nx run-many \ @@ -775,7 +780,7 @@ pnpm nx run-many \ Expected: all affected lint targets pass. -- [ ] **Step 3: Run build and atomized E2E** +- [x] **Step 3: Run build and atomized E2E** ```bash pnpm nx build web @@ -784,7 +789,7 @@ pnpm nx run electron-backend-e2e:e2e-ci--src/live-panel-toggles.e2e.ts Expected: build and all panel E2E tests pass. -- [ ] **Step 4: Run repository policy checks** +- [x] **Step 4: Run repository policy checks** ```bash pnpm run i18n:check @@ -795,7 +800,7 @@ git diff --check origin/master...HEAD Expected: all checks exit 0. -- [ ] **Step 5: Perform running Electron visual/accessibility verification** +- [x] **Step 5: Perform running Electron visual/accessibility verification** Start `pnpm run serve:backend`, connect through the required Computer Use or Electron CDP workflow, and inspect M3U, Xtream, Stalker, Favorites, Recent, @@ -803,7 +808,7 @@ desktop, narrow, dark, and light states. Record exact target sizes, focus movement, absence of duplicate/no-op controls, reclaimed widths, and restore ordering. -- [ ] **Step 6: Review the complete diff against the design** +- [x] **Step 6: Review the complete diff against the design** ```bash git status --short @@ -816,7 +821,7 @@ Re-read every requirement in the design and verify a code path plus test covers it. Confirm no channel-row responsive selector and no external-player right-region sizing rule changed. -- [ ] **Step 7: Fix review findings with test-first cycles** +- [x] **Step 7: Fix review findings with test-first cycles** For every finding, add or tighten the closest failing test, run it RED, make the smallest correction, rerun GREEN, and commit with an appropriate conventional diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts index c2cd2f457..ccf3cb666 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.spec.ts @@ -1123,6 +1123,29 @@ describe('VideoPlayerComponent', () => { ); }); + it('does not enable master suppression when every panel is effectively hidden on mobile', async () => { + routeParams$.next({ id: playlistId(), view: 'groups' }); + livePanelState.hidePanel(LIVE_LAYOUT_PANEL.CHANNELS); + await renderSidebar(); + + breakpointState$.next({ + breakpoints: { '(max-width: 599px)': true }, + matches: true, + }); + fixture.detectChanges(); + + component.handleKeyPress( + new KeyboardEvent('keydown', { + key: 'b', + ctrlKey: true, + }) + ); + + expect(livePanelState.masterSuppressed()).toBe(false); + expect(livePanelState.groupsIntent()).toBe('expanded'); + expect(livePanelState.channelsIntent()).toBe('collapsed'); + }); + it('keeps loading controls wired to both effective panel states', async () => { routeParams$.next({ id: playlistId(), view: 'groups' }); channelsLoading.set(true); diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts index 2ace49345..91bebce51 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts @@ -961,7 +961,7 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { ) { event.preventDefault(); this.livePanelState.toggleMasterSuppression( - this.applicableLeftPanels() + this.effectivelyVisibleLeftPanels() ); return; } @@ -975,10 +975,15 @@ export class VideoPlayerComponent implements OnInit, OnDestroy { } } - private applicableLeftPanels(): readonly LiveLayoutPanel[] { - return this.groupsApplicable() - ? [LIVE_LAYOUT_PANEL.GROUPS, LIVE_LAYOUT_PANEL.CHANNELS] - : [LIVE_LAYOUT_PANEL.CHANNELS]; + private effectivelyVisibleLeftPanels(): readonly LiveLayoutPanel[] { + const panels: LiveLayoutPanel[] = []; + if (this.groupsPanelExpanded()) { + panels.push(LIVE_LAYOUT_PANEL.GROUPS); + } + if (this.channelsPanelExpanded()) { + panels.push(LIVE_LAYOUT_PANEL.CHANNELS); + } + return panels; } /** diff --git a/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.spec.ts b/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.spec.ts index 561380b62..485b48974 100644 --- a/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.spec.ts +++ b/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.spec.ts @@ -17,9 +17,9 @@ describe('LiveLayoutPanelStateService', () => { expect(service.groupsIntent()).toBe('expanded'); expect(service.channelsIntent()).toBe('expanded'); - expect( - localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY) - ).toBe('expanded'); + expect(localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY)).toBe( + 'expanded' + ); expect( localStorage.getItem(LIVE_CHANNELS_PANEL_STATE_STORAGE_KEY) ).toBe('expanded'); @@ -38,14 +38,8 @@ describe('LiveLayoutPanelStateService', () => { }); it('keeps valid new keys and migrates only a missing or invalid sibling', () => { - localStorage.setItem( - LIVE_GROUPS_PANEL_STATE_STORAGE_KEY, - 'expanded' - ); - localStorage.setItem( - LIVE_CHANNELS_PANEL_STATE_STORAGE_KEY, - 'hidden' - ); + localStorage.setItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY, 'expanded'); + localStorage.setItem(LIVE_CHANNELS_PANEL_STATE_STORAGE_KEY, 'hidden'); localStorage.setItem(LIVE_SIDEBAR_STATE_STORAGE_KEY, 'collapsed'); const service = createService(); @@ -76,9 +70,9 @@ describe('LiveLayoutPanelStateService', () => { expect(service.groupsIntent()).toBe('collapsed'); expect(service.channelsIntent()).toBe('expanded'); - expect( - localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY) - ).toBe('collapsed'); + expect(localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY)).toBe( + 'collapsed' + ); expect( localStorage.getItem(LIVE_CHANNELS_PANEL_STATE_STORAGE_KEY) ).toBe('expanded'); @@ -110,33 +104,33 @@ describe('LiveLayoutPanelStateService', () => { }) ).toBe(false); expect(service.groupsIntent()).toBe('expanded'); - expect( - localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY) - ).toBe('expanded'); + expect(localStorage.getItem(LIVE_GROUPS_PANEL_STATE_STORAGE_KEY)).toBe( + 'expanded' + ); }); it('temporarily suppresses applicable visible panels and restores their persisted intents', () => { const service = createService(); service.hidePanel('channels'); - service.toggleMasterSuppression(['groups', 'channels']); + service.toggleMasterSuppression(['groups']); expect(service.masterSuppressed()).toBe(true); - expect( - service.isPanelExpanded('groups', { applicable: true }) - ).toBe(false); + expect(service.isPanelExpanded('groups', { applicable: true })).toBe( + false + ); expect(service.groupsIntent()).toBe('expanded'); expect(service.channelsIntent()).toBe('collapsed'); - service.toggleMasterSuppression(['groups', 'channels']); + service.toggleMasterSuppression([]); expect(service.masterSuppressed()).toBe(false); - expect( - service.isPanelExpanded('groups', { applicable: true }) - ).toBe(true); - expect( - service.isPanelExpanded('channels', { applicable: true }) - ).toBe(false); + expect(service.isPanelExpanded('groups', { applicable: true })).toBe( + true + ); + expect(service.isPanelExpanded('channels', { applicable: true })).toBe( + false + ); }); it('exits master suppression before applying a panel-local action', () => { @@ -155,14 +149,14 @@ describe('LiveLayoutPanelStateService', () => { expect(service.groupsIntent()).toBe('collapsed'); }); - it('leaves master suppression off when no applicable intent is visible', () => { + it('leaves master suppression off when no panel is effectively visible', () => { const service = createService(); - service.hidePanel('groups'); - service.hidePanel('channels'); - service.toggleMasterSuppression(['groups', 'channels']); + service.toggleMasterSuppression([]); expect(service.masterSuppressed()).toBe(false); + expect(service.groupsIntent()).toBe('expanded'); + expect(service.channelsIntent()).toBe('expanded'); }); }); diff --git a/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.ts b/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.ts index 48b423597..3b03e23e3 100644 --- a/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.ts +++ b/libs/portal/shared/data-access/src/lib/live-layout-panel-state.service.ts @@ -16,8 +16,7 @@ export type LiveLayoutPanel = export type LivePanelState = LiveSidebarState; -export const LIVE_GROUPS_PANEL_STATE_STORAGE_KEY = - 'live-groups-panel-state'; +export const LIVE_GROUPS_PANEL_STATE_STORAGE_KEY = 'live-groups-panel-state'; export const LIVE_CHANNELS_PANEL_STATE_STORAGE_KEY = 'live-channels-panel-state'; @@ -63,17 +62,14 @@ export class LiveLayoutPanelStateService { } toggleMasterSuppression( - applicablePanels: readonly LiveLayoutPanel[] + effectivelyVisiblePanels: readonly LiveLayoutPanel[] ): void { if (this._masterSuppressed()) { this._masterSuppressed.set(false); return; } - const hasVisiblePanel = applicablePanels.some( - (panel) => this.intentFor(panel)() === 'expanded' - ); - this._masterSuppressed.set(hasVisiblePanel); + this._masterSuppressed.set(effectivelyVisiblePanels.length > 0); } private setPanelIntent( @@ -97,9 +93,7 @@ export class LiveLayoutPanelStateService { } function restoreLiveLeftPanelIntents(): LiveLeftPanelIntents { - const legacyValue = localStorage.getItem( - LIVE_SIDEBAR_STATE_STORAGE_KEY - ); + const legacyValue = localStorage.getItem(LIVE_SIDEBAR_STATE_STORAGE_KEY); const legacyFallback = isLiveSidebarState(legacyValue) ? legacyValue : DEFAULT_LIVE_SIDEBAR_STATE; diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts index cb13a3780..363de53da 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts @@ -538,9 +538,9 @@ export class UnifiedCollectionPageComponent implements AfterContentInit { } event.preventDefault(); - this.livePanelState.toggleMasterSuppression([ - LIVE_LAYOUT_PANEL.CHANNELS, - ]); + this.livePanelState.toggleMasterSuppression( + this.channelsPanelExpanded() ? [LIVE_LAYOUT_PANEL.CHANNELS] : [] + ); } setFavSortMode(mode: FavoritesChannelSortMode): void { diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts index 51a3fbbfa..8acaffca1 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.spec.ts @@ -6,6 +6,7 @@ import { output, signal, } from '@angular/core'; +import { BreakpointObserver } from '@angular/cdk/layout'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { MatSnackBar } from '@angular/material/snack-bar'; @@ -682,6 +683,24 @@ describe('StalkerLiveStreamLayoutComponent', () => { expect(livePanelState.masterSuppressed()).toBe(false); }); + it('ignores the shortcut when compact Groups and collapsed Channels leave no visible panel', () => { + const livePanelState = TestBed.inject(LiveLayoutPanelStateService); + livePanelState.hidePanel(LIVE_LAYOUT_PANEL.CHANNELS); + jest.spyOn( + TestBed.inject(BreakpointObserver), + 'isMatched' + ).mockReturnValue(true); + fixture.detectChanges(); + + component.handleSidebarShortcut( + new KeyboardEvent('keydown', { ctrlKey: true, key: 'b' }) + ); + + expect(livePanelState.masterSuppressed()).toBe(false); + expect(livePanelState.groupsIntent()).toBe('expanded'); + expect(livePanelState.channelsIntent()).toBe('collapsed'); + }); + it('reuses a pending playback resolution during double-click activation', async () => { settingsStore.openStreamOnDoubleClick.set(true); portalPlayer.isEmbeddedPlayer.mockReturnValue(false); diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts index 758e2fd92..2ce4511c8 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.ts @@ -1,4 +1,5 @@ import { NgTemplateOutlet } from '@angular/common'; +import { BreakpointObserver } from '@angular/cdk/layout'; import { ChangeDetectionStrategy, ChangeDetectorRef, @@ -57,6 +58,7 @@ import { import { LiveEpgPanelSummary } from '@iptvnator/ui/shared-portals'; import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; import { + type LiveLayoutPanel, LiveLayoutPanelStateService, LIVE_LAYOUT_PANEL, } from '@iptvnator/portal/shared/data-access'; @@ -86,6 +88,7 @@ type StalkerPlayableChannel = StalkerPortalItem & { /** Channels rendered per "page" when the full list is served from the cache. */ const FULL_LIST_RENDER_CHUNK = 100; +const WORKSPACE_GROUPS_COMPACT_QUERY = '(max-width: 1023px)'; @Component({ selector: 'app-stalker-live-stream-layout', @@ -123,6 +126,7 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { private readonly translate = inject(TranslateService); private readonly livePanelState = inject(LiveLayoutPanelStateService); private readonly hostElement = inject>(ElementRef); + private readonly breakpointObserver = inject(BreakpointObserver); private readonly logger = createLogger('StalkerLiveStream'); readonly selectedCategoryTitle = this.stalkerStore.getSelectedCategoryName; @@ -778,13 +782,29 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy { ) { event.preventDefault(); this.livePanelState.toggleMasterSuppression( - this.channelsPanelApplicable() - ? [LIVE_LAYOUT_PANEL.GROUPS, LIVE_LAYOUT_PANEL.CHANNELS] - : [LIVE_LAYOUT_PANEL.GROUPS] + this.effectivelyVisibleLeftPanels() ); } } + private effectivelyVisibleLeftPanels(): readonly LiveLayoutPanel[] { + const panels: LiveLayoutPanel[] = []; + if ( + this.livePanelState.isPanelExpanded(LIVE_LAYOUT_PANEL.GROUPS, { + applicable: true, + responsiveSuppressed: this.breakpointObserver.isMatched( + WORKSPACE_GROUPS_COMPACT_QUERY + ), + }) + ) { + panels.push(LIVE_LAYOUT_PANEL.GROUPS); + } + if (this.channelsPanelExpanded()) { + panels.push(LIVE_LAYOUT_PANEL.CHANNELS); + } + return panels; + } + onLiveEpgDateNavigation(direction: EpgDateNavigationDirection): void { this.selectedLiveEpgDate.set( shiftEpgDateKey(this.selectedLiveEpgDate(), direction) diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts index aed76cb01..fb0606a3b 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts @@ -1,4 +1,5 @@ import { NgTemplateOutlet } from '@angular/common'; +import { BreakpointObserver } from '@angular/cdk/layout'; import { ChangeDetectionStrategy, Component, @@ -22,6 +23,7 @@ import { MatTooltipModule } from '@angular/material/tooltip'; import { TranslatePipe } from '@ngx-translate/core'; import { ResizableDirective } from '@iptvnator/ui/components'; import { + type LiveLayoutPanel, LiveLayoutPanelStateService, LIVE_LAYOUT_PANEL, } from '@iptvnator/portal/shared/data-access'; @@ -75,6 +77,7 @@ import { RuntimeCapabilitiesService, SettingsStore } from '@iptvnator/services'; import { LiveStreamAutoOpenStateService } from './live-stream-auto-open-state.service'; const LIVE_CHANNEL_SORT_STORAGE_KEY = 'xtream-live-channel-sort-mode'; +const WORKSPACE_GROUPS_COMPACT_QUERY = '(max-width: 1023px)'; interface XtreamLiveChannelItem { readonly added?: string; @@ -126,6 +129,7 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy { private readonly portalPlayer = inject(PORTAL_PLAYER); private readonly livePanelState = inject(LiveLayoutPanelStateService); private readonly liveAutoOpenState = inject(LiveStreamAutoOpenStateService); + private readonly breakpointObserver = inject(BreakpointObserver); readonly categories = this.xtreamStore.getCategoriesBySelectedType; readonly categoryItemCounts = this.xtreamStore.getCategoryItemCounts; @@ -559,13 +563,29 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy { ) { event.preventDefault(); this.livePanelState.toggleMasterSuppression( - this.showLiveChannelSidebar() - ? [LIVE_LAYOUT_PANEL.GROUPS, LIVE_LAYOUT_PANEL.CHANNELS] - : [LIVE_LAYOUT_PANEL.GROUPS] + this.effectivelyVisibleLeftPanels() ); } } + private effectivelyVisibleLeftPanels(): readonly LiveLayoutPanel[] { + const panels: LiveLayoutPanel[] = []; + if ( + this.livePanelState.isPanelExpanded(LIVE_LAYOUT_PANEL.GROUPS, { + applicable: true, + responsiveSuppressed: this.breakpointObserver.isMatched( + WORKSPACE_GROUPS_COMPACT_QUERY + ), + }) + ) { + panels.push(LIVE_LAYOUT_PANEL.GROUPS); + } + if (this.channelsPanelExpanded()) { + panels.push(LIVE_LAYOUT_PANEL.CHANNELS); + } + return panels; + } + onLiveEpgDateNavigation(direction: EpgDateNavigationDirection): void { this.selectedLiveEpgDate.set( shiftEpgDateKey(this.selectedLiveEpgDate(), direction)