feat(epg): show active catchup playback state

This commit is contained in:
4gray committed 2026-06-27 12:27:10 +02:00
1 parent d43bc83531
commit d9999d1e7f
25 files changed
+352 -10

No files matched your search

+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "تشغيل الأرشيف",
"RETURN_TO_LIVE": "العودة إلى البث المباشر",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "تشغيل الأرشيف",
"RETURN_TO_LIVE": "رجع للبث المباشر",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Прайграванне архіва",
"RETURN_TO_LIVE": "Вярнуцца да эфіру",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Archivwiedergabe",
"RETURN_TO_LIVE": "Zurück zum Live-TV",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Αναπαραγωγή αρχείου",
"RETURN_TO_LIVE": "Επιστροφή στη ζωντανή ροή",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Archive playback",
"RETURN_TO_LIVE": "Return to live",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Reproducción de archivo",
"RETURN_TO_LIVE": "Volver al directo",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Lecture archive",
"RETURN_TO_LIVE": "Retour au direct",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Riproduzione archivio",
"RETURN_TO_LIVE": "Torna alla diretta",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "アーカイブ再生",
"RETURN_TO_LIVE": "ライブに戻る",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "아카이브 재생",
"RETURN_TO_LIVE": "라이브로 돌아가기",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Archief afspelen",
"RETURN_TO_LIVE": "Terug naar live",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Odtwarzanie archiwum",
"RETURN_TO_LIVE": "Wróć do transmisji na żywo",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Reprodução do arquivo",
"RETURN_TO_LIVE": "Voltar ao vivo",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Архивная запись",
"RETURN_TO_LIVE": "Вернуться к эфиру",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "Arşiv oynatma",
"RETURN_TO_LIVE": "Canlı yayına dön",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "回看播放",
"RETURN_TO_LIVE": "返回直播",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
+2
View File
@@ -599,6 +599,8 @@
"TRUST_TLS_HOST_TITLE": "Trust invalid certificate?",
"TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.",
"CURRENT_PROGRAM": "Current program",
"ARCHIVE_PLAYBACK": "回看播放",
"RETURN_TO_LIVE": "返回直播",
"COLLAPSE_PANEL": "Collapse EPG panel",
"EXPAND_PANEL": "Expand EPG panel"
},
@@ -117,12 +117,15 @@
[collapsed]="isLiveEpgPanelCollapsed()"
[summary]="liveEpgPanelSummary()"
[loading]="isLoadingEpg()"
[summaryLabelKey]="liveEpgPanelSummaryLabelKey()"
[showDateNavigator]="true"
[selectedDate]="selectedLiveEpgDate()"
[showReturnToLive]="showReturnToLive()"
(collapsedChange)="
onLiveEpgPanelCollapsedChange($event)
"
(dateNavigation)="onLiveEpgDateNavigation($event)"
(returnToLive)="returnToLivePlayback()"
>
<ng-container
*ngTemplateOutlet="
@@ -113,7 +113,16 @@ class StubEpgListComponent {
selector: 'app-live-epg-panel',
standalone: true,
template: `
<div class="live-epg-panel-label">{{ summaryLabelKey() }}</div>
<div class="live-epg-panel-summary">{{ summary()?.title }}</div>
<button
class="live-epg-panel-return"
type="button"
[hidden]="!showReturnToLive()"
(click)="returnToLive.emit()"
>
Return to live
</button>
<ng-content />
`,
})
@@ -121,10 +130,13 @@ class StubLiveEpgPanelComponent {
readonly collapsed = input(false);
readonly summary = input<LiveEpgPanelSummary | null>(null);
readonly loading = input(false);
readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM');
readonly showDateNavigator = input(false);
readonly selectedDate = input<string | null>(null);
readonly showReturnToLive = input(false);
readonly collapsedChange = output<boolean>();
readonly dateNavigation = output<'next' | 'prev'>();
readonly returnToLive = output<void>();
}
@Directive({
@@ -685,6 +697,10 @@ describe('LiveStreamLayoutComponent', () => {
fixture.nativeElement.querySelector('.live-epg-panel-summary')
.textContent
).toContain('Current Show');
expect(
fixture.nativeElement.querySelector('.live-epg-panel-label')
.textContent
).toContain('EPG.CURRENT_PROGRAM');
});
it('uses currentEpgItem instead of assuming the first schedule item is current', () => {
@@ -762,6 +778,159 @@ describe('LiveStreamLayoutComponent', () => {
'Channel 101 - Archived Show',
'channel-101.png'
);
expect(component.activePlayback()).toEqual(
expect.objectContaining({
streamUrl: 'https://example.com/timeshift.ts',
isLive: false,
})
);
});
it('shows the active archive program in the live EPG panel summary', async () => {
const archivedProgram: EpgProgram = {
start: '2026-04-04T10:00:00.000Z',
stop: '2026-04-04T11:00:00.000Z',
channel: 'channel-101',
title: 'Archived Show',
desc: null,
category: null,
startTimestamp: 1775296800,
stopTimestamp: 1775300400,
};
epgItems.set([
buildEpgItem(
'archived',
'Archived Show',
archivedProgram.start,
archivedProgram.stop
),
]);
currentEpgItem.set(
buildEpgItem(
'current',
'Current Show',
'2026-04-05T11:30:00.000Z',
'2026-04-05T12:30:00.000Z'
)
);
component.playLive(sampleChannel);
await component.onProgramActivated({
type: 'timeshift',
program: archivedProgram,
});
fixture.detectChanges();
const panel = fixture.debugElement.query(
By.directive(StubLiveEpgPanelComponent)
);
expect(panel.componentInstance.summary()).toEqual(
expect.objectContaining({
title: 'Archived Show',
start: '2026-04-04T10:00:00.000Z',
stop: '2026-04-04T11:00:00.000Z',
})
);
expect(panel.componentInstance.summaryLabelKey()).toBe(
'EPG.ARCHIVE_PLAYBACK'
);
expect(panel.componentInstance.showReturnToLive()).toBe(true);
expect(
fixture.nativeElement.querySelector('.live-epg-panel-summary')
.textContent
).toContain('Archived Show');
expect(
fixture.nativeElement.querySelector('.live-epg-panel-summary')
.textContent
).not.toContain('Current Show');
});
it('returns archive playback to the selected live stream from the panel action', async () => {
const archivedProgram: EpgProgram = {
start: '2026-04-04T10:00:00.000Z',
stop: '2026-04-04T11:00:00.000Z',
channel: 'channel-101',
title: 'Archived Show',
desc: null,
category: null,
startTimestamp: 1775296800,
stopTimestamp: 1775300400,
};
currentEpgItem.set(
buildEpgItem(
'current',
'Current Show',
'2026-04-05T11:30:00.000Z',
'2026-04-05T12:30:00.000Z'
)
);
component.playLive(sampleChannel);
await component.onProgramActivated({
type: 'timeshift',
program: archivedProgram,
});
fixture.detectChanges();
const panel = fixture.debugElement.query(
By.directive(StubLiveEpgPanelComponent)
);
panel.componentInstance.returnToLive.emit();
fixture.detectChanges();
expect(component.activeCatchupProgram()).toBeNull();
expect(component.activePlayback()).toEqual(
expect.objectContaining({
streamUrl: 'https://example.com/live.ts',
isLive: true,
})
);
expect(panel.componentInstance.summary()?.title).toBe('Current Show');
expect(panel.componentInstance.summaryLabelKey()).toBe(
'EPG.CURRENT_PROGRAM'
);
expect(panel.componentInstance.showReturnToLive()).toBe(false);
});
it('publishes the active archive program in remote-control status', async () => {
const archivedProgram: EpgProgram = {
start: '2026-04-04T10:00:00.000Z',
stop: '2026-04-04T11:00:00.000Z',
channel: 'channel-101',
title: 'Archived Show',
desc: null,
category: null,
startTimestamp: 1775296800,
stopTimestamp: 1775300400,
};
currentEpgItem.set(
buildEpgItem(
'current',
'Current Show',
'2026-04-05T11:30:00.000Z',
'2026-04-05T12:30:00.000Z'
)
);
const updateRemoteControlStatus = window.electron
?.updateRemoteControlStatus as jest.Mock;
component.playLive(sampleChannel);
fixture.detectChanges();
updateRemoteControlStatus.mockClear();
await component.onProgramActivated({
type: 'timeshift',
program: archivedProgram,
});
fixture.detectChanges();
expect(updateRemoteControlStatus).toHaveBeenLastCalledWith(
expect.objectContaining({
epgTitle: 'Archived Show',
epgStart: '2026-04-04T10:00:00.000Z',
epgEnd: '2026-04-04T11:00:00.000Z',
})
);
});
it('passes the active catchup program to the EPG list until live playback resumes', async () => {
@@ -177,6 +177,7 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
this.epgItems().map((program) => this.toControlledEpgProgram(program))
);
private readonly currentTimeMs = signal(Date.now());
readonly activeCatchupProgram = signal<EpgProgram | null>(null);
readonly controlledArchiveDays = computed(() =>
Math.max(
0,
@@ -215,7 +216,17 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
);
readonly isSidebarCollapsed = this.liveSidebarStateService.isCollapsed;
readonly liveEpgPanelSummary = computed(() =>
this.toLiveEpgPanelSummary(this.currentEpgItem())
this.toLiveEpgPanelSummary(
this.activeCatchupProgram() ?? this.currentEpgItem()
)
);
readonly liveEpgPanelSummaryLabelKey = computed(() =>
this.activeCatchupProgram()
? 'EPG.ARCHIVE_PLAYBACK'
: 'EPG.CURRENT_PROGRAM'
);
readonly showReturnToLive = computed(
() => this.activeCatchupProgram() !== null
);
readonly liveChannelSortLabel = computed(() =>
getPortalChannelSortModeLabel(this.liveChannelSortMode())
@@ -268,7 +279,6 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
readonly activeStreamUrl = computed(
() => this.activePlayback()?.streamUrl ?? ''
);
readonly activeCatchupProgram = signal<EpgProgram | null>(null);
favorites = new Map<number, boolean>();
constructor() {
@@ -353,7 +363,8 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
const selectedContentType = this.xtreamStore.selectedContentType();
const selectedItem = this.xtreamStore.selectedItem();
const channels = this.getVisibleChannels();
const currentProgram = this.currentEpgItem();
const activeProgram =
this.activeCatchupProgram() ?? this.currentEpgItem();
if (selectedContentType !== 'live' || !selectedItem?.xtream_id) {
remoteControl.updateRemoteControlStatus({
@@ -374,9 +385,9 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
isLiveView: true,
channelName: selectedItem.title ?? selectedItem.name,
channelNumber: currentIndex >= 0 ? currentIndex + 1 : undefined,
epgTitle: currentProgram?.title,
epgStart: currentProgram?.start,
epgEnd: currentProgram?.stop ?? currentProgram?.end,
epgTitle: activeProgram?.title,
epgStart: activeProgram?.start,
epgEnd: this.getProgramStop(activeProgram),
supportsVolume: false,
});
});
@@ -518,6 +529,15 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
this.selectedLiveEpgDate.set(selectedDate);
}
returnToLivePlayback(): void {
const selectedItem = this.selectedLiveItem();
if (!selectedItem?.xtream_id) {
return;
}
this.playLive(selectedItem, true);
}
ngOnDestroy(): void {
this.unsubscribeRemoteChannelChange?.();
this.unsubscribeRemoteCommand?.();
@@ -640,6 +660,7 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
streamUrl: catchupUrl,
title: this.getCatchupPlaybackTitle(item, program),
thumbnail: item.poster_url ?? item.stream_icon ?? null,
isLive: false,
});
if (this.usesEmbeddedPlayer()) {
return;
@@ -672,7 +693,7 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
}
private toLiveEpgPanelSummary(
program: EpgItem | null | undefined
program: EpgItem | EpgProgram | null | undefined
): LiveEpgPanelSummary | null {
if (!program) {
return null;
@@ -681,10 +702,23 @@ export class LiveStreamLayoutComponent implements OnInit, OnDestroy {
return {
title: program.title,
start: program.start,
stop: program.stop ?? program.end,
stop: this.getProgramStop(program),
};
}
private getProgramStop(
program: EpgItem | EpgProgram | null | undefined
): string | undefined {
if (!program) {
return undefined;
}
return (
program.stop ??
('end' in program ? (program.end ?? undefined) : undefined)
);
}
private get remoteControlBridge(): Window['electron'] | undefined {
return this.runtime.supportsRemoteControl ? window.electron : undefined;
}
@@ -22,7 +22,7 @@
<div class="live-epg-panel__summary" role="status">
<span class="live-epg-panel__label">
{{ 'EPG.CURRENT_PROGRAM' | translate }}
{{ summaryLabelKey() | translate }}
</span>
<div class="live-epg-panel__program-line">
@@ -58,6 +58,22 @@
</div>
</div>
@if (showReturnToLive()) {
<button
mat-button
class="live-epg-panel__return-live"
type="button"
(click)="returnToLive.emit()"
[attr.aria-label]="'EPG.RETURN_TO_LIVE' | translate"
[matTooltip]="'EPG.RETURN_TO_LIVE' | translate"
>
<mat-icon>live_tv</mat-icon>
<span class="live-epg-panel__return-live-text">
{{ 'EPG.RETURN_TO_LIVE' | translate }}
</span>
</button>
}
@if (showDateNavigator()) {
@if (!collapsed()) {
<div
@@ -16,7 +16,7 @@
.live-epg-panel__summary-bar {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr) auto;
grid-template-columns: auto minmax(0, 1fr) auto auto;
align-items: center;
column-gap: 8px;
min-height: 56px;
@@ -95,6 +95,45 @@
color: var(--mat-sys-on-surface-variant);
}
.live-epg-panel__return-live {
min-width: 0;
height: 36px;
padding-inline: 10px 12px;
border-radius: 10px;
color: var(--app-selection-color);
background: color-mix(
in srgb,
var(--app-selection-color) 10%,
transparent
);
app-region: no-drag;
mat-icon {
flex-shrink: 0;
width: 18px;
height: 18px;
font-size: 18px;
}
.live-epg-panel__return-live-text {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.78rem;
font-weight: 700;
}
&:hover {
color: var(--mat-sys-on-primary-container);
background: color-mix(
in srgb,
var(--app-selection-color) 16%,
transparent
);
}
}
.live-epg-panel__date-nav {
display: flex;
align-items: center;
@@ -230,6 +269,17 @@
}
}
@media only screen and (max-width: 640px) {
.live-epg-panel__return-live {
width: 36px;
padding-inline: 0;
.live-epg-panel__return-live-text {
display: none;
}
}
}
@media only screen and (max-width: 560px) {
.live-epg-panel__label {
display: none;
@@ -15,10 +15,13 @@ import {
[collapsed]="collapsed"
[summary]="summary"
[loading]="loading"
[summaryLabelKey]="summaryLabelKey"
[showDateNavigator]="showDateNavigator"
[selectedDate]="selectedDate"
[showReturnToLive]="showReturnToLive"
(collapsedChange)="collapsed = $event"
(dateNavigation)="dateDirection = $event"
(returnToLive)="returnToLiveCount = returnToLiveCount + 1"
>
<div class="projected-content">Projected EPG</div>
</app-live-epg-panel>
@@ -27,9 +30,12 @@ import {
class HostComponent {
collapsed = false;
loading = false;
summaryLabelKey = 'EPG.CURRENT_PROGRAM';
showDateNavigator = false;
showReturnToLive = false;
selectedDate = '2026-04-05';
dateDirection: 'next' | 'prev' | null = null;
returnToLiveCount = 0;
summary: LiveEpgPanelSummary | null = {
title: 'Current Show',
start: '2026-04-05T11:30:00.000Z',
@@ -67,6 +73,10 @@ describe('LiveEpgPanelComponent', () => {
fixture.nativeElement.querySelector('.live-epg-panel__title')
.textContent
).toContain('Current Show');
expect(
fixture.nativeElement.querySelector('.live-epg-panel__label')
.textContent
).toContain('EPG.CURRENT_PROGRAM');
expect(
fixture.nativeElement.querySelector(
'.live-epg-panel__progress-fill'
@@ -139,4 +149,25 @@ describe('LiveEpgPanelComponent', () => {
fixture.nativeElement.querySelector('.live-epg-panel__date-nav')
).toBeNull();
});
it('renders archive playback state and emits return-to-live requests', () => {
fixture.componentInstance.summaryLabelKey = 'EPG.ARCHIVE_PLAYBACK';
fixture.componentInstance.showReturnToLive = true;
fixture.detectChanges();
expect(
fixture.nativeElement.querySelector('.live-epg-panel__label')
.textContent
).toContain('EPG.ARCHIVE_PLAYBACK');
const returnButton = fixture.nativeElement.querySelector(
'.live-epg-panel__return-live'
) as HTMLButtonElement | null;
expect(returnButton).not.toBeNull();
expect(returnButton?.textContent).toContain('EPG.RETURN_TO_LIVE');
returnButton?.click();
expect(fixture.componentInstance.returnToLiveCount).toBe(1);
});
});
@@ -48,10 +48,13 @@ export class LiveEpgPanelComponent {
readonly collapsed = input(false);
readonly summary = input<LiveEpgPanelSummary | null>(null);
readonly loading = input(false);
readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM');
readonly showDateNavigator = input(false);
readonly selectedDate = input<string | null>(null);
readonly showReturnToLive = input(false);
readonly collapsedChange = output<boolean>();
readonly dateNavigation = output<EpgDateNavigationDirection>();
readonly returnToLive = output<void>();
private readonly translate = inject(TranslateService);
private readonly currentTimeMs = signal(Date.now());