diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 5a000420d..71fb991c6 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index 4f9ba1acc..b371d0f4d 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index cc570b499..df7327856 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 101a56113..117b00e85 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index ad7c709e3..cdb79fd75 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 505e9b732..ab750db4d 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index 399cc65b8..c5e6613f8 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index b8442a44f..f3b44c878 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index d52e18696..c430ff912 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index f6a216345..38fb527a8 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index b06585608..861dd3073 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index b4ad2049b..f1c8b2ec3 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index ee6f76613..df24c4bac 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index e617dde18..bcb91447a 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 35f25f1ea..2f18227eb 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 21040126d..952073f9d 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 0b8f7606e..70f0d7e64 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -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" }, diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index ed249feea..e3d4e7db2 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -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" }, diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html index cc104d7f2..7819c4f66 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.html @@ -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()" > {{ summaryLabelKey() }}
{{ summary()?.title }}
+ `, }) @@ -121,10 +130,13 @@ class StubLiveEpgPanelComponent { readonly collapsed = input(false); readonly summary = input(null); readonly loading = input(false); + readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM'); readonly showDateNavigator = input(false); readonly selectedDate = input(null); + readonly showReturnToLive = input(false); readonly collapsedChange = output(); readonly dateNavigation = output<'next' | 'prev'>(); + readonly returnToLive = output(); } @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 () => { 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 a7657337b..a28d14b54 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 @@ -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(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(null); favorites = new Map(); 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; } diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html index 98bf21751..768fe12b4 100644 --- a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html +++ b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html @@ -22,7 +22,7 @@
- {{ 'EPG.CURRENT_PROGRAM' | translate }} + {{ summaryLabelKey() | translate }}
@@ -58,6 +58,22 @@
+ @if (showReturnToLive()) { + + } + @if (showDateNavigator()) { @if (!collapsed()) {
Projected EPG
@@ -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); + }); }); diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts index 4b1e4f0cd..8053dded3 100644 --- a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts +++ b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts @@ -48,10 +48,13 @@ export class LiveEpgPanelComponent { readonly collapsed = input(false); readonly summary = input(null); readonly loading = input(false); + readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM'); readonly showDateNavigator = input(false); readonly selectedDate = input(null); + readonly showReturnToLive = input(false); readonly collapsedChange = output(); readonly dateNavigation = output(); + readonly returnToLive = output(); private readonly translate = inject(TranslateService); private readonly currentTimeMs = signal(Date.now());