mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(epg): show active catchup playback state
This commit is contained in:
1 parent
d43bc83531
commit
d9999d1e7f
25 files changed
+352
-10
No files matched your search
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
+3
@@ -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="
|
||||
|
||||
+169
@@ -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 () => {
|
||||
|
||||
+42
-8
@@ -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());
|
||||
|
||||
Reference in new issue
Block a user