diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.spec.ts b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.spec.ts index 47bd26b81..6b5d53e2b 100644 --- a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.spec.ts +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.spec.ts @@ -2,7 +2,12 @@ import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; import { EMPTY, of, throwError } from 'rxjs'; import { EpgService } from '@iptvnator/epg/data-access'; -import type { EpgProgram, PlaylistMeta } from '@iptvnator/shared/interfaces'; +import { + DEFAULT_DASHBOARD_RAILS_SETTINGS, + type EpgProgram, + type PlaylistMeta, + type PortalActivityItem, +} from '@iptvnator/shared/interfaces'; import { SettingsStore } from '@iptvnator/services'; import { DashboardDataService } from '@iptvnator/workspace/dashboard/data-access'; import { DashboardLiveEpgPresenter } from './dashboard-live-epg.presenter'; @@ -37,6 +42,10 @@ describe('DashboardLiveEpgPresenter', () => { let presenter: DashboardLiveEpgPresenter; let getCurrentProgramsForChannels: jest.Mock; let playlists: ReturnType>; + let recentItems: ReturnType>; + let dashboardRails: ReturnType< + typeof signal + >; /** Portal answers are the sibling presenter's job; stub it out here. */ let portal: { connect: jest.Mock; @@ -64,6 +73,8 @@ describe('DashboardLiveEpgPresenter', () => { { _id: 'portal', serverUrl: 'http://portal' } as PlaylistMeta, ]); + recentItems = signal([]); + dashboardRails = signal({ ...DEFAULT_DASHBOARD_RAILS_SETTINGS }); portal = { connect: jest.fn(), setPinnedKeys: jest.fn(), @@ -84,7 +95,7 @@ describe('DashboardLiveEpgPresenter', () => { useValue: { playlists, // The presenter also derives the portal source list. - globalRecentItems: signal([]), + globalRecentItems: recentItems, globalFavoriteLiveItems: signal([]), globalRecentLiveItems: signal([]), }, @@ -97,7 +108,7 @@ describe('DashboardLiveEpgPresenter', () => { provide: SettingsStore, useValue: { resolvedEpgOffsetMinutes: () => 0, - dashboardRails: signal(undefined), + dashboardRails, }, }, ], @@ -166,6 +177,32 @@ describe('DashboardLiveEpgPresenter', () => { ); }); + it('pins the hero row only while the hero rail shows one', () => { + const heroLive = { + id: 'x-7', + title: 'Hero channel', + type: 'live', + source: 'xtream', + playlist_id: 'p', + category_id: '1', + xtream_id: 7, + } as PortalActivityItem; + recentItems.set([heroLive]); + TestBed.tick(); + + expect(portal.setPinnedKeys).toHaveBeenLastCalledWith(['xtream::p::7']); + + // With the rail hidden nothing is pinned: the first portal row is + // then a favourite, and pinning it would keep asking for a card + // nobody can see. + dashboardRails.set({ + ...DEFAULT_DASHBOARD_RAILS_SETTINGS, + hero: false, + }); + TestBed.tick(); + expect(portal.setPinnedKeys).toHaveBeenLastCalledWith([null]); + }); + it('prefers the portal answer and forwards what the portal presenter owns', () => { const xmltvCard = card({ id: 'x', diff --git a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.ts b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.ts index 977ed2cfe..70baea7e6 100644 --- a/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.ts +++ b/libs/workspace/dashboard/feature/src/lib/rails/dashboard-live-epg.presenter.ts @@ -19,12 +19,12 @@ import { switchMap, } from 'rxjs'; import { EpgService } from '@iptvnator/epg/data-access'; -import type { - EpgProgram, - PortalActivityItem, +import { + normalizeDashboardRailsSettings, + type EpgProgram, + type PortalActivityItem, } from '@iptvnator/shared/interfaces'; import { SettingsStore } from '@iptvnator/services'; -import { normalizeDashboardRailsSettings } from '@iptvnator/shared/interfaces'; import { normalizeEpgUrls } from '@iptvnator/shared/m3u-utils'; import { buildDashboardPortalLiveEpgKey, @@ -131,17 +131,25 @@ export class DashboardLiveEpgPresenter { { initialValue: new Map() } ); + private readonly rails = computed(() => + normalizeDashboardRailsSettings(this.settingsStore.dashboardRails?.()) + ); + + /** The live row behind the hero panel, when that rail shows one. */ + private readonly heroLiveItem = computed(() => { + const hero = this.data.globalRecentItems()[0] ?? null; + return this.rails().hero && hero?.type === 'live' ? hero : null; + }); + // The Xtream/Stalker live rows behind the hero and the two live rails. // Their programmes come from the portal, asked for lazily per visible // card; M3U rows stay on the XMLTV batch above. private readonly portalItems = computed( () => { - const rails = normalizeDashboardRailsSettings( - this.settingsStore.dashboardRails?.() - ); - const hero = this.data.globalRecentItems()[0] ?? null; + const rails = this.rails(); + const hero = this.heroLiveItem(); return [ - ...(rails.hero && hero?.type === 'live' ? [hero] : []), + ...(hero ? [hero] : []), ...(rails.liveFavorites ? this.data .globalFavoriteLiveItems() @@ -160,12 +168,12 @@ export class DashboardLiveEpgPresenter { this.portal.connect(this.portalItems); // The hero sits at the top of the page and is never scrolled into // view, so its key is wanted regardless of what the rails report. + // Only the hero: the first entry of `portalItems` is a favourite + // when that rail is hidden, and pinning it would keep asking for a + // card nobody can see. effect(() => { - const [first] = this.portalItems(); - const heroKey = - first?.type === 'live' - ? buildDashboardPortalLiveEpgKey(first) - : null; + const hero = this.heroLiveItem(); + const heroKey = hero ? buildDashboardPortalLiveEpgKey(hero) : null; untracked(() => this.portal.setPinnedKeys([heroKey])); }); }