mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
fix(dashboard): pin only the hero row for portal EPG
The consolidation derived the pinned key from the first entry of the portal source list, which is a favourite when the hero rail is hidden. Pinning that keeps asking for a card nobody can see. The hero row is now its own computed, gated on the rail like the source list is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9c5c256b0d
commit
1a2e25645d
2 files changed
+62
-17
No files matched your search
+40
-3
@@ -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<typeof signal<PlaylistMeta[]>>;
|
||||
let recentItems: ReturnType<typeof signal<PortalActivityItem[]>>;
|
||||
let dashboardRails: ReturnType<
|
||||
typeof signal<typeof DEFAULT_DASHBOARD_RAILS_SETTINGS>
|
||||
>;
|
||||
/** 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<PortalActivityItem[]>([]);
|
||||
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',
|
||||
|
||||
@@ -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<string, EpgProgram | null>() }
|
||||
);
|
||||
|
||||
private readonly rails = computed(() =>
|
||||
normalizeDashboardRailsSettings(this.settingsStore.dashboardRails?.())
|
||||
);
|
||||
|
||||
/** The live row behind the hero panel, when that rail shows one. */
|
||||
private readonly heroLiveItem = computed<PortalActivityItem | null>(() => {
|
||||
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<readonly PortalActivityItem[]>(
|
||||
() => {
|
||||
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]));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user